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

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

第 1/3 篇

「在 DoEasy 里给图形做动画切图」

DoEasy 函数库做到第七十八部分时,已经把图形对象的动画原理铺开了,核心思路是把一张大图按帧切分成小块,再逐帧贴到图表上形成动效。这套机制不依赖外部 GIF,全部用库内对象和资源管理实现,MT5 图表上跑起来延迟可控。 原文给出的示例发布于 2021 年 8 月 12 日 11:08,作者 Artyom Trishkin,截至文档统计该帖浏览量 3767、评论 0,说明这类底层切图玩法在普通用户里还偏冷门。 想验证的话,直接在 MT5 打开 DoEasy 对应版本,找到图形资源类里负责复制粘贴图像部分的实现,把一张横排序列帧图塞进去,看图表对象是否按索引切片刷新。外汇与贵金属图表加载自定义图形属高风险操作环境,参数没配对可能导致对象重绘卡顿。

灵动动画的最小单元:背景存取与重绘

图形界面里显示的数据大多会随时间变化,GUI 元素需要用视觉效果响应用户操作。若直接在画布上逐帧绘制静态图像而不做清理,各帧会彼此重叠成一团乱码——我曾把同一段文本画在会话窗不同坐标,结果就是一堆错位像素。 避免混乱有两种路数。其一是在每帧重绘前彻底擦掉旧图、重铺背景再画新内容,但这只适合尺寸小、结构简单的会话窗;其二是把叠加图像那块的背景暂存进数组,移动时先用存好的背景覆盖旧位置“复原”,再于新坐标绘制,相当于背景在内存里被持续周转。 我引入函数库的最小动画元素只有三步:在指定坐标保存背景、在该坐标显示图像、重绘时恢复背景。为此写了一个轻量类,负责存坐标与尺寸,并提供“按坐标存背景”和“将背景写入数组”两个方法,尺寸和坐标随背景一并留在类变量里。 之所以用类而非给会话窗挂两个方法,是因为单文本或单图时尚可应付;若会话窗多处同时跑动画,每个实例自理坐标与背景会更干净。一个实例就是一条可加入列表动态处理的图像序列,后续也能拿已绘内容当背景继续叠加。

◍ 给图形元素类加像素快照能力

做 MT5 自定义 GUI 时,最烦的是在画布上叠了文本或图像之后,想恢复原背景却只能重绘一遍。重绘慢且容易闪,更直接的办法是在对象刚创建、外观定型后,把 CCanvas 的图形资源像素整块读进数组存着,以后要恢复只需数组间复制。 在 GCnvElement.mqh 的受保护区,我加了一个数组用于缓存初始像素,以及一个把 CCanvas 实例图形资源存进数组的方法。代价是占一点内存,但换来了随时一键还原界面元素原始外观的可能。

文本绘制有 9 个锚点(如 RightBottom 是右下角起始),而函数库初始坐标统一是矩形左上角 LeftTop。若直接按初始坐标存图,文本会跑到保存图像的右下角,背景区域对不上。所以必须先用 CCanvas 的 TextSize() 算轮廓宽高,再按对齐方式算 X/Y 偏移量,把背景正确存进数组。

私密区另存了最后绘制文本的 X、Y 坐标变量,在公开区提供返回指针和保存图像的方法。下面这段消息索引是配套加进 Data.mqh 的,其中高亮部分是给图形元素和像素复制器类新补的报错文本:

MQL5 / C++
class=class="str">"cmt">//--- CChartObjCollection
  MSG_CHART_COLLECTION_TEXT_CHART_COLLECTION,        class=class="str">"cmt">// Chart collection
  MSG_CHART_COLLECTION_ERR_FAILED_CREATE_CHART_OBJ,  class=class="str">"cmt">// Failed to create a new chart object
  MSG_CHART_COLLECTION_ERR_FAILED_ADD_CHART,         class=class="str">"cmt">// Failed to add a chart object to the collection
  MSG_CHART_COLLECTION_ERR_CHARTS_MAX,               class=class="str">"cmt">// Cannot open new chart. Number of open charts at maximum
  MSG_CHART_COLLECTION_CHART_OPENED,                 class=class="str">"cmt">// Chart opened
  MSG_CHART_COLLECTION_CHART_CLOSED,                 class=class="str">"cmt">// Chart closed
  MSG_CHART_COLLECTION_CHART_SYMB_CHANGED,           class=class="str">"cmt">// Chart symbol changed
  MSG_CHART_COLLECTION_CHART_TF_CHANGED,             class=class="str">"cmt">// Chart timeframe changed
  MSG_CHART_COLLECTION_CHART_SYMB_TF_CHANGED,        class=class="str">"cmt">// Chart symbol and timeframe changed
class=class="str">"cmt">//--- CGCnvElement
  MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY,                  class=class="str">"cmt">// Error! Empty array

class=class="str">"cmt">//--- CForm
  MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT,class=class="str">"cmt">// No shadow object. Create it using the CreateShadowObj() method
  MSG_FORM_OBJECT_ERR_FAILED_CREATE_SHADOW_OBJ,      class=class="str">"cmt">// Failed to create new shadow object
  MSG_FORM_OBJECT_ERR_FAILED_CREATE_PC_OBJ,          class=class="str">"cmt">// Failed to create new pixel copier object
  MSG_FORM_OBJECT_PC_OBJ_ALREADY_IN_LIST,            class=class="str">"cmt">// Pixel copier object with ID already present in the list 
  MSG_FORM_OBJECT_PC_OBJ_NOT_EXIST_LIST,             class=class="str">"cmt">// No pixel copier object with ID in the list 
class=class="str">"cmt">//--- CShadowObj
  MSG_SHADOW_OBJ_IMG_SMALL_BLUR_LARGE,               class=class="str">"cmt">// Error! Image size too small or blur too extensive

  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- 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"},

「图形元素类的内部报错与成员布局」

在 MQL5 自定义图形库里,CGCnvElement 这类图形元素对象把运行时提示做成了俄英双语文案表,方便调试时直接定位状态。比如图表事件会返回「Open chart / Closed chart」以及「Changed chart symbol」「Changed chart timeframe」,而像素拷贝器出错则提示「Failed to create new pixel copier object」或「There is already a pixel copier object in the list with ID 」。 从类声明看,CGCnvElement 继承自 CGBaseObj,保护成员里挂了 CCanvas 做绘制、CPause 做停顿控制,还有 m_shadow 标记是否带阴影、m_chart_color_bg 存图表背景色。值得注意的是 m_data_array[] 这个 uint 数组,专门用来缓存资源数据的副本,若传入空数组会触发「Error! Empty array」的硬报错。 图像尺寸异常也会直接抛错:当模糊半径过大或图像太小,类会返回「Error! Image size is very small or very large blur」。开 MT5 把这段报错表接进你的 EA 日志,能省掉一半图形对象联调时间;外汇与贵金属图表上做这类自定义控件,需注意滑点与时延带来的高风险。

MQL5 / C++
class CGCnvElement : class="kw">public CGBaseObj
  {
class="kw">protected:
  CCanvas          m_canvas;             class=class="str">"cmt">// CCanvas class object
  CPause           m_pause;              class=class="str">"cmt">// Pause class object
  class="type">bool             m_shadow;             class=class="str">"cmt">// Shadow presence
  class="type">color            m_chart_color_bg;     class=class="str">"cmt">// Chart background class="type">color
  class="type">uint             m_data_array[];       class=class="str">"cmt">// Array for storing resource data copy
class=class="str">"cmt">//--- Return the cursor position relative to the(class="num">1) entire element and(class="num">2) the element&class="macro">#x27;s active area
  class="type">bool             CursorInsideElement(class="kw">const class="type">int x,class="kw">const class="type">int y);
  class="type">bool             CursorInsideActiveArea(class="kw">const class="type">int x,class="kw">const class="type">int y);
class=class="str">"cmt">//--- Create(class="num">1) the object structure and(class="num">2) the object from the structure
  class="kw">virtual class="type">bool     ObjectToStruct(class="type">void);
  class="kw">virtual class="type">void     StructToObject(class="type">void);

class=class="str">"cmt">//--- Save the graphical resource to the array

画布元素基类的属性与虚函数骨架

在 MT5 自定义图形库里,CGCnvElement 作为画布元素的基类,用三块定长数组分别承接订单的整数、实数与字符串属性:m_long_prop[ORDER_PROP_INTEGER_TOTAL]、m_double_prop[ORDER_PROP_DOUBLE_TOTAL]、m_string_prop[ORDER_PROP_STRING_TOTAL]。这种分桶存储让后续派生类按属性类型直接索引,不必做运行时类型判断。 文本锚点相关字段暴露了渲染细节:m_text_anchor 控制对齐方式,m_text_x 与 m_text_y 记录上一次绘制的像素坐标,m_color_bg 与 m_opacity 管背景色和透明度(0~255 的 uchar)。你在调面板透明度时改的就是 m_opacity。 虚函数 SupportProperty 的三个重载直接决定了对象能力边界——整数与字符串属性返回 true,双精度属性返回 false。这意味着基类默认不支持 double 类属性,若你的标签要显示浮点价格,得在派生类里重写这个重载。 比较与持久化接口给了列表排序和存取可能:Compare 按 mode 做多属性比对,IsEqual 做全等判断,Save 配合 file_handle 写盘。下面这段声明是基类成员的原貌,逐行拆完就能照抄进自己的 EA 面板。

MQL5 / C++
class="type">bool ResourceCopy(class="kw">const class="type">class="kw">string source);
class="kw">private:
  class="type">long m_long_prop[ORDER_PROP_INTEGER_TOTAL];      class=class="str">"cmt">// Integer properties
  class="type">class="kw">double m_double_prop[ORDER_PROP_DOUBLE_TOTAL];   class=class="str">"cmt">// Real properties
  class="type">class="kw">string m_string_prop[ORDER_PROP_STRING_TOTAL];   class=class="str">"cmt">// String properties

  ENUM_TEXT_ANCHOR m_text_anchor;                  class=class="str">"cmt">// Current text alignment
  class="type">int m_text_x;                                    class=class="str">"cmt">// Text last X coordinate
  class="type">int m_text_y;                                    class=class="str">"cmt">// Text last Y coordinate
  class="type">color m_color_bg;                                class=class="str">"cmt">// Element background class="type">color
  class="type">uchar m_opacity;                                 class=class="str">"cmt">// Element opacity

class=class="str">"cmt">//--- Return the index of the array the order&class="macro">#x27;s(class="num">1) class="type">class="kw">double and(class="num">2) class="type">class="kw">string properties are located at
class=class="str">"cmt">//--- Return the flag of the object supporting this class="kw">property
  class="kw">virtual class="type">bool SupportProperty(ENUM_CANV_ELEMENT_PROP_INTEGER class="kw">property)    { class="kw">return true;   }
  class="kw">virtual class="type">bool SupportProperty(ENUM_CANV_ELEMENT_PROP_DOUBLE class="kw">property)     { class="kw">return false;  }
  class="kw">virtual class="type">bool SupportProperty(ENUM_CANV_ELEMENT_PROP_STRING class="kw">property)     { class="kw">return true;   }
class=class="str">"cmt">//--- Return itself
  CGCnvElement *GetObject(class="type">void)                                              { class="kw">return &this;  }
class=class="str">"cmt">//--- Compare CGCnvElement objects with each other by all possible properties(for sorting the lists by a specified 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=class="str">"cmt">//--- Compare CGCnvElement objects with each other by all properties(to search equal objects)
  class="type">bool IsEqual(CGCnvElement* compared_obj) class="kw">const;
class=class="str">"cmt">//--- (class="num">1) Save the object to file and(class="num">2) upload the object from the file
  class="kw">virtual class="type">bool Save(class="kw">const class="type">int file_handle);

◍ 图形元素类的接口暴露了什么

在自定义图形库里,一个元素对象对外只靠几个虚函数和 inline 方法撑起生命周期。Load 是虚函数,接收文件句柄把资源读进实例,派生类可以重写它来决定怎么解析二进制。 Create 负责在指定图表子窗口里落位:chart_id 定位图表,wnd_num 是子窗口序号,name 做唯一标识,x/y/w/h 是像素级矩形,colour 和 opacity 控制填充与透明度,redraw 默认 false 避免每次创建都重绘。 GetCanvasObj 直接返回内部 CCanvas 的指针,意味着你拿到的是 &this.m_canvas,可绕过封装往画布写像素。SetFrequency 把等待毫秒塞给 m_pause.SetWaitingMSC,这决定了回调刷新的节流间隔,比如传 100 就倾向每 100ms 重算一次。 Move 用新 x/y 平移整个画布,ImageCopy 把 named source 的位图拷进 uint 数组,这两步是做动态贴图和水印的基础。外汇与贵金属图表叠加自绘层波动剧烈,高杠杆下误绘可能放大误判,验证前请在模拟盘跑通。

MQL5 / C++
class="kw">virtual class="type">bool        Load(class="kw">const class="type">int file_handle);

class=class="str">"cmt">//--- Create the element
   class="type">bool               Create(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,
                             class="kw">const class="type">uchar opacity,
                             class="kw">const class="type">bool redraw=false);

class=class="str">"cmt">//--- Return the pointer to a canvas object
   CCanvas           *GetCanvasObj(class="type">void)                              { class="kw">return &this.m_canvas; }
class=class="str">"cmt">//--- Set the canvas update frequency
   class="type">void               SetFrequency(class="kw">const class="type">class="kw">ulong value)                 { this.m_pause.SetWaitingMSC(value); }
class=class="str">"cmt">//--- Update the coordinates(shift the canvas)
   class="type">bool               Move(class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">bool redraw=false);
class=class="str">"cmt">//--- Save an image to the array
   class="type">bool               ImageCopy(class="kw">const class="type">class="kw">string source,class="type">uint &array[]);

常见问题

先给图形元素类加上像素快照(背景存取)能力,把当前画布存下来,重绘时再贴回,避免整屏闪烁。
看类内部报错接口返回的错码定位;成员布局乱会导致快照坐标错位、重绘偏移,需按基类骨架规整属性。
可以,小布能读取你的图形元素类结构,标出未做像素快照或重绘逻辑缺失的位置,并给出修改建议。
不实现虚函数,派生类无法被统一调度,动画切图时不会触发对应帧更新,元素静止或报错。
接口本身不拖慢,但每帧调用过多暴露方法会增加开销;只暴露切图与重绘必需接口即可。