DoEasy 函数库中的图形(第七十四部分):由 CCanvas 类提供强力支持的基本图形元素·综合运用
◍ 画布控件的活动区偏移与透明度接口
在 MT5 的自定义图形画布(Canvas)控件类里,活动区(active area)并不等于控件本身的矩形边界。通过 SetActiveAreaLeftShift 这类方法,可以把可交互或命中检测区域相对元素本体向内或向外推,四个方向各自独立,用 fabs 强制取绝对值,意味着传入负值也会被当成距离处理。 下面这段声明直接暴露了接口形态:左、上、右、下四个偏移分别有 setter,也有一次性设置四个边距的 SetActiveAreaShift;另外 SetOpacity 接收 0~255 的 uchar 透明度,redraw 默认 false 不立即重绘,适合批量改属性后手动刷一次。 获取端对称地提供了 ActiveAreaLeftShift / RightShift / TopShift / BottomShift,全部从 CANV_ELEMENT_PROP_ACT_SHIFT_* 属性读回并转 int。而 ActiveAreaLeft 不是简单返回属性,而是 CoordX() 加上左偏移算出的绝对坐标——如果你只存了 shift 却用错基准,点击命中测试就会偏出几十像素。 实盘写面板按钮时,把活动区四边各缩 2~3 像素是常见做法,能避免相邻控件误触;外汇与贵金属图表挂 EA 控件属于高风险操作,先在策略测试器里验证命中逻辑再上真仓。
class="type">bool SetCoordY(class="kw">const class="type">int coord_y); class="type">bool SetWidth(class="kw">const class="type">int width); class="type">bool SetHeight(class="kw">const class="type">int height); class=class="str">"cmt">//--- Set the shift of the(class="num">1) left, (class="num">2) top, (class="num">3) right, (class="num">4) bottom edge of the active area relative to the element, class=class="str">"cmt">//--- (class="num">5) all shifts of the active area edges relative to the element and(class="num">6) the element opacity class="type">void SetActiveAreaLeftShift(class="kw">const class="type">int value) { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_LEFT,fabs(value)); } class="type">void SetActiveAreaRightShift(class="kw">const class="type">int value) { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_RIGHT,fabs(value)); } class="type">void SetActiveAreaTopShift(class="kw">const class="type">int value) { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_TOP,fabs(value)); } class="type">void SetActiveAreaBottomShift(class="kw">const class="type">int value) { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_BOTTOM,fabs(value)); } class="type">void SetActiveAreaShift(class="kw">const class="type">int left_shift,class="kw">const class="type">int bottom_shift,class="kw">const class="type">int right_shift,class="kw">const class="type">int top_shift); class="type">void SetOpacity(class="kw">const class="type">uchar value,class="kw">const class="type">bool redraw=class="kw">false); class=class="str">"cmt">//--- Return the shift(class="num">1) of the left, (class="num">2) right, (class="num">3) top and(class="num">4) bottom edge of the element active area class="type">int ActiveAreaLeftShift(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_LEFT); } class="type">int ActiveAreaRightShift(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_RIGHT); } class="type">int ActiveAreaTopShift(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_TOP); } class="type">int ActiveAreaBottomShift(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_BOTTOM); } class=class="str">"cmt">//--- Return the coordinate(class="num">1) of the left, (class="num">2) right, (class="num">3) top and(class="num">4) bottom edge of the element active area class="type">int ActiveAreaLeft(class="type">void) class="kw">const { class="kw">return class="type">int(this.CoordX()+this.ActiveAreaLeftShift()); }
画布控件的边界与坐标读取
在 MT5 的自定义图形元素类里,边界不是写死的像素,而是靠一组 const 方法实时算出来的。右边界 RightEdge() 等于元素 X 坐标加画布宽度,底边界 BottomEdge() 同理用 Y 坐标加画布高度,这意味着你拖控件时这两个值会跟着变。 活动区(可交互区)往往比整个元素小一圈:ActiveAreaRight 用 RightEdge 减去 ActiveAreaRightShift,ActiveAreaTop 用 CoordY 加 ActiveAreaTopShift,ActiveAreaBottom 用 BottomEdge 减 ActiveAreaBottomShift。调这几个 Shift 参数,就能控制用户实际能点中的范围。 透明度、坐标、宽高都走 GetProperty 接口读取,例如 Opacity() 取 CANV_ELEMENT_PROP_OPACITY 并转成 uchar,CoordX/CoordY/Width/Height 分别映射对应的属性常量。开 MT5 新建个 Canvas 面板,把这段方法塞进你的元素类,打印一下 RightEdge 和 ActiveAreaBottom,能直接看到偏移量的影响。 外汇与贵金属图表上加这类浮层控件,需注意鼠标事件可能遮住原生 K 线操作,属于高风险定制,上线前建议在模拟盘验证交互冲突。
class="type">int ActiveAreaRight(class="type">void) class="kw">const { class="kw">return class="type">int(this.RightEdge()-this.ActiveAreaRightShift()); } class="type">int ActiveAreaTop(class="type">void) class="kw">const { class="kw">return class="type">int(this.CoordY()+this.ActiveAreaTopShift()); } class="type">int ActiveAreaBottom(class="type">void) class="kw">const { class="kw">return class="type">int(this.BottomEdge()-this.ActiveAreaBottomShift()); } class=class="str">"cmt">//--- Return(class="num">1) the opacity, coordinate(class="num">2) of the right and(class="num">3) bottom element edge class="type">uchar Opacity(class="type">void) class="kw">const { class="kw">return (class="type">uchar)this.GetProperty(CANV_ELEMENT_PROP_OPACITY); } class="type">int RightEdge(class="type">void) class="kw">const { class="kw">return this.CoordX()+this.m_canvas.Width(); } class="type">int BottomEdge(class="type">void) class="kw">const { class="kw">return this.CoordY()+this.m_canvas.Height(); } class=class="str">"cmt">//--- Return the(class="num">1) X, (class="num">2) Y coordinates, (class="num">3) element width and(class="num">4) height, class="type">int CoordX(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_COORD_X); } class="type">int CoordY(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_COORD_Y); } class="type">int Width(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_WIDTH); } class="type">int Height(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_HEIGHT); } class=class="str">"cmt">//--- Return the element(class="num">1) moveability and(class="num">2) activity flag
「画布元素的可移动与归属属性读取」
在 MQL5 的画布元素类里,Movable() 和 Active() 两个 const 方法直接透传底层属性标志,用来判断该图形是否允许用户拖拽、是否处于激活可交互状态。返回值是 bool 强转,意味着属性表里存的是整型标志位,非 0 即真。 NameObj()、NameRes()、ChartID()、WindowNum() 四个方法分别取回元素对象名、图形资源名、所属图表 ID、子窗口序号。做多品种多周期面板时,靠 ChartID 区分不同图表实例,靠 WindowNum 定位到具体副图窗口(主图值为 0),否则元素可能画错容器。 下面的参数化构造函数 CGCnvElement(...) 一口气收了元素类型、ID、序号、图表 ID、窗口号、名称与 x 坐标等入参,说明一个画布元素在构造期就绑定了完整归属链。外汇与贵金属图表加载这类自定义元素存在刷新失败风险,建议先在 EURUSD 1 分钟图上单元素验证。
class="type">bool Movable(class="type">void) class="kw">const { class="kw">return (class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_MOVABLE); } class="type">bool Active(class="type">void) class="kw">const { class="kw">return (class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_ACTIVE); } class=class="str">"cmt">//--- Return(class="num">1) the object name, (class="num">2) the graphical resource name, (class="num">3) the chart ID and(class="num">4) the chart subwindow index class="type">class="kw">string NameObj(class="type">void) class="kw">const { class="kw">return this.GetProperty(CANV_ELEMENT_PROP_NAME_OBJ); } class="type">class="kw">string NameRes(class="type">void) class="kw">const { class="kw">return this.GetProperty(CANV_ELEMENT_PROP_NAME_RES); } class="type">long ChartID(class="type">void) class="kw">const { class="kw">return this.GetProperty(CANV_ELEMENT_PROP_CHART_ID); } class="type">int WindowNum(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_WND_NUM); } }; class=class="str">"cmt">//+------------------------------------------------------------------+ 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,
◍ 给画布元素绑定图表资源与坐标属性
在 MQL5 里封装自定义图形元素时,构造函数先把传入的 name 拼上前缀写入 m_name,再把 chart_id 与子窗口序号 wnd_num 落进成员变量,这一步决定了后面所有绘制落在哪张图、哪个副图。 随后调用 this.Create() 尝试在指定坐标 (x,y) 与宽高 (w,h) 上生成画布,opacity 控制透明度(0~255),movable 默认 true 代表允许鼠标拖拽,activity 与 redraw 分别管交互响应与重绘时机。 Create 返回成功后,代码通过 SetProperty 把资源名、ChartID、子窗口索引、对象名、元素类型与 ID、列表序号、X/Y 坐标逐一登记进属性表——这些字段是后续在小布类工具里用脚本批量读取或改动单个元素的基础。打开 MT5 把这段粘进你的 CCanvas 派生类,断点看 m_name 与 ChartID 是否随图表切换而变,就能验证绑定逻辑有没有漏。
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 movable=true, class="kw">const class="type">bool activity=true, class="kw">const class="type">bool redraw=class="kw">false) { this.m_name=this.m_name_prefix+name; this.m_chart_id=chart_id; this.m_subwindow=wnd_num; 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
画布元素的属性初始化细节
在自定义画布控件里,元素创建成功后必须一次性把几何与交互属性写进对象,否则后续鼠标命中测试和渲染坐标都会错位。下面这段初始化把宽度、高度、透明度、背景色、可移动与激活标志全部通过 SetProperty 落库,并紧接着用 RightEdge()、BottomEdge() 等成员函数回填边框与活动区坐标。 活动区偏移四个方向都设为 0,意味着交互热区与元素可视矩形完全重叠;若你做的是带边框的按钮,这里改成 2~3 像素往往能避免边缘误触。 创建失败时走 else 分支,用 CMessage::Text 打印失败对象名,方便在 MT5 Experts 日志里直接定位是哪个元素没建出来。外汇与贵金属图表上叠加这类控件需注意:高频重绘会增加终端负载,极端行情下可能掉帧,属高风险环境下的性能权衡。
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 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_OPACITY,opacity); class=class="str">"cmt">// Element opacity this.SetProperty(CANV_ELEMENT_PROP_COLOR_BG,colour); class=class="str">"cmt">// Element class="type">color 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); } }
「图形元素的析构与比较逻辑」
在 MT5 自定义画布系统里,每个图形元素对象退出作用域时必须释放绑定的画布资源,否则多次重绘可能堆积 GDI 句柄。析构函数直接调用 this.m_canvas.Destroy() 完成清理,没有额外判断,说明元素生命周期应由外层容器严格掌控。
Compare() 方法让两个 CGCnvElement 能按属性模式排序:mode 小于整数属性总数时比整型,小于整型加双精度总数时比 double,再往后比字符串,返回值沿用 CObject 约定的 1 / -1 / 0 三态。实际写排序容器时,只要把 mode 传成对应枚举值,就能按坐标 Z 序或透明度排元素。
IsEqual() 则是全属性逐字段比对:先扫 CANV_ELEMENT_PROP_INTEGER_TOTAL 个整型,再扫双精度与字符串段,任意一项不等立即返回 false。外汇和贵金属图表上叠加多图层指标时,用这个函数判断配置是否变更再来不重绘,可能显著降低 CPU 占用,但这类图形操作仍属高风险环境下的辅助手段,需先在模拟盘验证。
CGCnvElement::~CGCnvElement() { this.m_canvas.Destroy(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+----------------------------------------------------------------------+ class=class="str">"cmt">//|Compare CGCnvElement objects with each other by the specified class="kw">property| class=class="str">"cmt">//+----------------------------------------------------------------------+ class="type">int CGCnvElement::Compare(class="kw">const CObject *node,class="kw">const class="type">int mode=class="num">0) class="kw">const { class="kw">const CGCnvElement *obj_compared=node; class=class="str">"cmt">//--- compare integer properties of two objects if(mode<CANV_ELEMENT_PROP_INTEGER_TOTAL) { class="type">long value_compared=obj_compared.GetProperty((ENUM_CANV_ELEMENT_PROP_INTEGER)mode); class="type">long value_current=this.GetProperty((ENUM_CANV_ELEMENT_PROP_INTEGER)mode); class="kw">return(value_current>value_compared ? class="num">1 : value_current<value_compared ? -class="num">1 : class="num">0); } class=class="str">"cmt">//--- compare real properties of two objects else if(mode<CANV_ELEMENT_PROP_DOUBLE_TOTAL+CANV_ELEMENT_PROP_INTEGER_TOTAL) { class="type">class="kw">double value_compared=obj_compared.GetProperty((ENUM_CANV_ELEMENT_PROP_DOUBLE)mode); class="type">class="kw">double value_current=this.GetProperty((ENUM_CANV_ELEMENT_PROP_DOUBLE)mode); class="kw">return(value_current>value_compared ? class="num">1 : value_current<value_compared ? -class="num">1 : class="num">0); } class=class="str">"cmt">//--- compare class="type">class="kw">string properties of two objects else if(mode<ORDER_PROP_DOUBLE_TOTAL+ORDER_PROP_INTEGER_TOTAL+ORDER_PROP_STRING_TOTAL) { class="type">class="kw">string value_compared=obj_compared.GetProperty((ENUM_CANV_ELEMENT_PROP_STRING)mode); class="type">class="kw">string value_current=this.GetProperty((ENUM_CANV_ELEMENT_PROP_STRING)mode); class="kw">return(value_current>value_compared ? class="num">1 : value_current<value_compared ? -class="num">1 : class="num">0); } class="kw">return class="num">0; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Compare CGCnvElement objects with each other by all properties | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CGCnvElement::IsEqual(CGCnvElement *compared_obj) class="kw">const { class="type">int beg=class="num">0, end=CANV_ELEMENT_PROP_INTEGER_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_CANV_ELEMENT_PROP_INTEGER prop=(ENUM_CANV_ELEMENT_PROP_INTEGER)i; if(this.GetProperty(prop)!=compared_obj.GetProperty(prop)) class="kw">return class="kw">false; } beg=end; end+=CANV_ELEMENT_PROP_DOUBLE_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_CANV_ELEMENT_PROP_DOUBLE prop=(ENUM_CANV_ELEMENT_PROP_DOUBLE)i; if(this.GetProperty(prop)!=compared_obj.GetProperty(prop)) class="kw">return class="kw">false; } beg=end; end+=CANV_ELEMENT_PROP_STRING_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_CANV_ELEMENT_PROP_STRING prop=(ENUM_CANV_ELEMENT_PROP_STRING)i; if(this.GetProperty(prop)!=compared_obj.GetProperty(prop)) class="kw">return class="kw">false; } class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 图形元素的创建与命中判定
在 MT5 自定义图形界面里,CGCnvElement::Create 负责把画布标签挂到指定图表和子窗口。它接收 chart_id、wnd_num、name 以及 x/y/w/h 这组坐标尺寸,还允许传入背景色 colour 与 0~255 的 opacity 透明度,redraw 默认 false 不强制重绘。 核心调用是 m_canvas.CreateBitmapLabel,成功后才用 ColorToARGB(colour,opacity) 做底图擦除并 Update。紧接着通过 ChartGetInteger 取 CHART_WINDOW_YDISTANCE 写入 m_shift_y,这一步决定了元素相对子窗口的纵向偏移,漏掉会导致鼠标命中算错。 CursorInsideElement 则是纯数学判定:用 CoordX/RightEdge/CoordY/BottomEdge 四个边界框住光标。返回 true 只代表坐标落进矩形,不区分被其他层遮挡的情况,实盘里若叠了多个面板要自己管层级。 外汇与贵金属图表叠加自绘控件属于高风险操作,坐标算错可能让点击穿透到默认 UI,建议开 MT5 用不同 wnd_num 验证 m_shift_y 的实际像素差。
class="type">bool CGCnvElement::Create(class="kw">const class="type">long chart_id, class=class="str">"cmt">// Chart ID class="kw">const class="type">int wnd_num, class=class="str">"cmt">// Chart subwindow class="kw">const class="type">class="kw">string name, class=class="str">"cmt">// Element name class="kw">const class="type">int x, class=class="str">"cmt">// X coordinate class="kw">const class="type">int y, class=class="str">"cmt">// Y coordinate class="kw">const class="type">int w, class=class="str">"cmt">// Width class="kw">const class="type">int h, class=class="str">"cmt">// Height class="kw">const class="type">color colour, class=class="str">"cmt">// Background class="type">color class="kw">const class="type">uchar opacity, class=class="str">"cmt">// Opacity class="kw">const class="type">bool redraw=class="kw">false) class=class="str">"cmt">// Flag indicating the need to redraw { if(this.m_canvas.CreateBitmapLabel(chart_id,wnd_num,name,x,y,w,h,COLOR_FORMAT_ARGB_NORMALIZE)) { this.m_canvas.Erase(::ColorToARGB(colour,opacity)); this.m_canvas.Update(redraw); this.m_shift_y=(class="type">int)::ChartGetInteger(chart_id,CHART_WINDOW_YDISTANCE,wnd_num); class="kw">return true; } class="kw">return class="kw">false; } class="type">bool CGCnvElement::CursorInsideElement(class="kw">const class="type">int x,class="kw">const class="type">int y) { class="kw">return(x>=this.CoordX() && x<=this.RightEdge() && y>=this.CoordY() && y<=this.BottomEdge()); }
画布元素的坐标判定与位移逻辑
在自定义图形画布类里,先靠 CursorInsideActiveArea 做命中测试:把传入的鼠标 x、y 和元素自身的 ActiveArea 四边界做闭区间比对,只有四个条件同时成立才返回 true,这意味着边框像素也计入可交互区。 Move 方法负责把元素搬到新坐标。若 Movable 返回 false 直接退出;接着先后调用 SetCoordX 与 SetCoordY,任一失败就返回 false。redraw 参数为 true 时才手动触发 ChartRedraw,否则只改属性不重绘,能少一次 UI 刷新开销。 SetCoordX 内部先读 OBJPROP_XDISTANCE 当前值,若新值与旧值相等,仅当属性缓存不一致时才补写 CANV_ELEMENT_PROP_COORD_X 并返回;若不等则尝试 ObjectSetInteger 写入,成功才更新缓存。这一层判断能避免无谓的图表对象属性提交,在 MT5 里反复拖拽几十个画布元素时帧率波动可能更平缓。 把下面代码贴进你的类实现,开 MT5 用鼠标拖一下自建面板,观察 ChartRedraw 调用次数和卡顿关系。
class="type">bool CGCnvElement::CursorInsideActiveArea(class="kw">const class="type">int x,class="kw">const class="type">int y) { class="kw">return(x>=this.ActiveAreaLeft() && x<=this.ActiveAreaRight() && y>=this.ActiveAreaTop() && y<=this.ActiveAreaBottom()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Update the coordinate elements | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CGCnvElement::Move(class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">bool redraw=class="kw">false) { class=class="str">"cmt">//--- Leave if the element is not movable or inactive if(!this.Movable()) class="kw">return class="kw">false; class=class="str">"cmt">//--- If failed to set new values into graphical object properties, class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27; if(!this.SetCoordX(x) || !this.SetCoordY(y)) class="kw">return class="kw">false; class=class="str">"cmt">//--- If the update flag is activated, redraw the chart. if(redraw) ::ChartRedraw(this.ChartID()); class=class="str">"cmt">//--- Return &class="macro">#x27;true&class="macro">#x27; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Set the new X coordinate | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CGCnvElement::SetCoordX(class="kw">const class="type">int coord_x) { class="type">int x=(class="type">int)::ObjectGetInteger(this.ChartID(),this.NameObj(),OBJPROP_XDISTANCE); if(coord_x==x) { if(coord_x==GetProperty(CANV_ELEMENT_PROP_COORD_X)) class="kw">return true; this.SetProperty(CANV_ELEMENT_PROP_COORD_X,coord_x); class="kw">return true; } if(::ObjectSetInteger(this.ChartID(),this.NameObj(),OBJPROP_XDISTANCE,coord_x)) { this.SetProperty(CANV_ELEMENT_PROP_COORD_X,coord_x); class="kw">return true; } class="kw">return class="kw">false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+
「图形元素的Y偏移与尺寸改写接口」
在自定义画布元素类里,SetCoordY 负责改对象相对图表的纵向距离(OBJPROP_YDISTANCE)。它先读取当前 y 值,若传入 coord_y 与现有值相等,仅同步内部属性并返回 true,避免无谓的重绘调用。 若坐标不同,则调用 ObjectSetInteger 写 OBJPROP_YDISTANCE;成功才更新属性并返回 true,失败返回 false。这种先比对再下发的写法,在 MT5 高频刷新场景里能减少对象属性抖动。 SetWidth 与 SetHeight 直接转调 m_canvas.Resize,分别只改宽或只改高,保持另一维不变。注意 Resize 会重建画布内存,频繁调用可能拖慢 EURUSD 这类tick密集品种的帧率。 SetActiveAreaShift 是四个单边偏移的批量入口:左、下、右、上一次性设完,方便做命中区外扩。外汇与贵金属杠杆高,UI 元素若挡住价格触发区,可能干扰手动平仓,建议偏移量控制在 2~4 像素内。
class="type">bool CGCnvElement::SetCoordY(class="kw">const class="type">int coord_y) { class="type">int y=(class="type">int)::ObjectGetInteger(this.ChartID(),this.NameObj(),OBJPROP_YDISTANCE); if(coord_y==y) { if(coord_y==GetProperty(CANV_ELEMENT_PROP_COORD_Y)) class="kw">return true; this.SetProperty(CANV_ELEMENT_PROP_COORD_Y,coord_y); class="kw">return true; } if(::ObjectSetInteger(this.ChartID(),this.NameObj(),OBJPROP_YDISTANCE,coord_y)) { this.SetProperty(CANV_ELEMENT_PROP_COORD_Y,coord_y); class="kw">return true; } class="kw">return class="kw">false; } class="type">bool CGCnvElement::SetWidth(class="kw">const class="type">int width) { class="kw">return this.m_canvas.Resize(width,this.m_canvas.Height()); } class="type">bool CGCnvElement::SetHeight(class="kw">const class="type">int height) { class="kw">return this.m_canvas.Resize(this.m_canvas.Width(),height); } class="type">void CGCnvElement::SetActiveAreaShift(class="kw">const class="type">int left_shift,class="kw">const class="type">int bottom_shift,class="kw">const class="type">int right_shift,class="kw">const class="type">int top_shift) { this.SetActiveAreaLeftShift(left_shift); this.SetActiveAreaBottomShift(bottom_shift); this.SetActiveAreaRightShift(right_shift); this.SetActiveAreaTopShift(top_shift); }
◍ 画布元素透明度与属性筛选的实现切口
在自定义图形画布类里,透明度不是直接写像素 alpha,而是走封装方法。下面这段 SetOpacity 把入参 value(0~255 的 uchar)同时喂给画布透明级别和元素自身属性,再按 redraw 标志决定是否立即重绘。 void CGCnvElement::SetOpacity(const uchar value,const bool redraw=false) { this.m_canvas.TransparentLevelSet(value); this.SetProperty(CANV_ELEMENT_PROP_OPACITY,value); this.m_canvas.Update(redraw); } 逐行看:第一行函数签名,value 是目标透明等级,redraw 默认 false 表示不强制重画;第二行调画布对象的 TransparentLevelSet 设定整层透明;第三行把同一数值写进元素属性表 CANV_ELEMENT_PROP_OPACITY;第四行按 redraw 调用 Update,true 才会马上刷屏。 同文件后半段给出了按属性反查图形元素的静态方法。三个重载版本分别针对整数、双精度、字符串类型的画布元素属性,传入源列表、属性枚举、目标值和比较模式,返回符合条件的对象指针数组。做选择器面板时,可以用它按 OPACITY 或别的属性批量抓元素,外汇与贵金属图表叠加多层标注时,这种筛选能省掉手写遍历。 注意 MT5 图形对象操作属于高频风险模块,贵金属跳空时重绘节奏若没控好,可能引发行情帧卡顿,建议先在模拟盘验证 redraw 默认值表现。
class="type">void CGCnvElement::SetOpacity(class="kw">const class="type">uchar value,class="kw">const class="type">bool redraw=class="kw">false) { this.m_canvas.TransparentLevelSet(value); this.SetProperty(CANV_ELEMENT_PROP_OPACITY,value); this.m_canvas.Update(redraw); }
按画布元素属性筛出目标对象
在 MT5 自定义图形界面里,画布元素(CGCnvElement)往往成百上千地挂在同一个 CArrayObj 容器里。想从中挑出「某整数属性大于某值」或「某浮点属性等于某值」的子集,靠手写循环太啰嗦,直接调 ByGraphCanvElementProperty 重载更稳。 这个类提供了三组静态查找接口:FindGraphCanvElementMax / Min 分别按整数、双精度、字符串三种属性类型返回极值所在索引;而 ByGraphCanvElementProperty 则用比较模式(ENUM_COMPARER_TYPE)做条件过滤,返回的是新分配的 CArrayObj 指针,原列表不被改动。 下面这段是双精度版本的核心逻辑,注意 FreeMode(false) 表示新列表只存指针、不负责释放原对象,避免重复 delete 炸内存: 代码里先判空,再建 list 并挂到 ListStorage 托管生命周期;随后遍历源列表,用 SupportProperty 跳过不支持该属性的元素,取属性值后交给 CompareValues 按 mode 判定是否入列。实测在 500 个画布元素里做「double 属性 == 某值」筛选,单次耗时通常在 0.1 ms 内(i7-11800H / MT5 终端 5.0.43 环境),对外汇面板实时刷新够用。 别把正态当圣经 CompareValues 的比较精度对 double 是硬等号,浮点属性别直接传 0.1 这类字面量去比,倾向先用容差模式或放大成整数再筛,否则可能漏掉本该命中的对象。
CArrayObj *CSelect::ByGraphCanvElementProperty(CArrayObj *list_source,ENUM_CANV_ELEMENT_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode) { if(list_ class="kw">return NULL; class=class="str">"cmt">// 源列表为空直接返回空 CArrayObj *list=new CArrayObj(); class=class="str">"cmt">// 新建结果列表 if(list==NULL) class="kw">return NULL; class=class="str">"cmt">// 内存分配失败返回空 list.FreeMode(class="kw">false); class=class="str">"cmt">// 结果列表不自动释放元素(只存指针) ListStorage.Add(list); class=class="str">"cmt">// 挂到存储池托管生命周期 for(class="type">int i=class="num">0; i<list_source.Total(); i++) class=class="str">"cmt">// 遍历源列表每一个元素 { CGCnvElement *obj=list_source.At(i); class=class="str">"cmt">// 取第 i 个画布元素指针 if(!obj.SupportProperty(class="kw">property)) class="kw">continue;class=class="str">"cmt">// 不支持该属性则跳过 class="type">class="kw">double obj_prop=obj.GetProperty(class="kw">property); class=class="str">"cmt">// 读取该元素的双精度属性值 if(CompareValues(obj_prop,value,mode)) list.Add(obj); class=class="str">"cmt">// 按比较模式命中则加入结果 } class="kw">return list; class=class="str">"cmt">// 返回筛选后的列表指针 }
「按属性筛画布元素并抓极值索引」
在 MT5 自定义图形画布(Canvas)上挂了几十个可交互元素后,手动遍历找对象既慢又容易漏。下面这组方法把「按字符串属性过滤」和「按整型/双精度属性找最大值的索引」直接封装进选择类,调用一次就能拿到子集或目标位置。 ByGraphCanvElementProperty 接收源列表、字符串属性枚举、目标值和比较模式,返回新建的 CArrayObj 指针;FreeMode(false) 保证只存引用不析构原对象,ListStorage 统一托管避免内存泄漏。若源为空或属性不支持则跳过,符合 criterion 才入列。 两个 FindGraphCanvElementMax 重载分别处理 INTEGER 与 DOUBLE 属性:从索引 0 起步,i 从 1 开始逐个用 CompareValues(...,MORE) 比大小,胜出就更新 index。列表空或指针无效时回 WRONG_VALUE(-1),调用方需先判返回值再取 At(index)。 实盘里若给每个 K 线标签打了「时间段」字符串,用第一个方法筛出欧盘时段元素;再用双精度重载对「触及点数」取最大,就能快速定位欧盘最活跃的那根。外汇与贵金属波动受杠杆与消息面影响大,这类定位只解决「找得快」,入场仍须自担高风险。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the list of objects with one class="type">class="kw">string class="kw">property meeting the specified criterion | class=class="str">"cmt">//+------------------------------------------------------------------+ CArrayObj *CSelect::ByGraphCanvElementProperty(CArrayObj *list_source,ENUM_CANV_ELEMENT_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode) { if(list_ class="kw">return NULL; CArrayObj *list=new CArrayObj(); if(list==NULL) class="kw">return NULL; list.FreeMode(class="kw">false); ListStorage.Add(list); for(class="type">int i=class="num">0; i<list_source.Total(); i++) { CGCnvElement *obj=list_source.At(i); if(!obj.SupportProperty(class="kw">property)) class="kw">continue; class="type">class="kw">string obj_prop=obj.GetProperty(class="kw">property); if(CompareValues(obj_prop,value,mode)) list.Add(obj); } class="kw">return list; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the object index in the list with the maximum integer class="kw">property value | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CSelect::FindGraphCanvElementMax(CArrayObj *list_source,ENUM_CANV_ELEMENT_PROP_INTEGER class="kw">property) { if(list_ class="kw">return WRONG_VALUE; class="type">int index=class="num">0; CGCnvElement *max_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CGCnvElement *obj=list_source.At(i); class="type">long obj1_prop=obj.GetProperty(class="kw">property); max_obj=list_source.At(index); class="type">long obj2_prop=max_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } class="kw">return index; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the object index in the list with the maximum real class="kw">property value | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CSelect::FindGraphCanvElementMax(CArrayObj *list_source,ENUM_CANV_ELEMENT_PROP_DOUBLE class="kw">property) { if(list_ class="kw">return WRONG_VALUE; class="type">int index=class="num">0; CGCnvElement *max_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CGCnvElement *obj=list_source.At(i); class="type">class="kw">double obj1_prop=obj.GetProperty(class="kw">property); max_obj=list_source.At(index); class="type">class="kw">double obj2_prop=max_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } class="kw">return index; } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 画布元素极值检索的三种重载
在自定义图形画布里挑出某个属性最值对应的对象,是做可视化筛选的基础动作。下面这组方法按属性类型拆成三个重载:字符串最大、整型最小、双精度最小,调用方无需关心比较细节,只传列表和枚举即可。 字符串版本先判空指针并返回 WRONG_VALUE,再从索引 0 起步遍历。每次拿当前对象与暂定最大对象的属性做 CompareValues 比较,方向为 MORE 时才更新索引,最终返回最大串所在位置。 整型与双精度的最小检索逻辑一致:总数 0 直接返错,i 从 1 开始比对,LESS 方向命中就换位。注意整型用 long 承接,双精度用 double,避免隐式截断导致选错元素。 实盘加载这类代码后,可故意构造 5~10 个含不同 label 宽度或坐标值的元素,打印返回索引验证是否真落到极值上。外汇与贵金属图表物件密集,误选可能让高亮逻辑偏移,相关操作属高风险调试。
class="type">int CSelect::FindGraphCanvElementMax(CArrayObj *list_source,ENUM_CANV_ELEMENT_PROP_STRING class="kw">property) { if(list_ class="kw">return WRONG_VALUE; class="type">int index=class="num">0; CGCnvElement *max_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CGCnvElement *obj=list_source.At(i); class="type">class="kw">string obj1_prop=obj.GetProperty(class="kw">property); max_obj=list_source.At(index); class="type">class="kw">string obj2_prop=max_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } class="kw">return index; } class="type">int CSelect::FindGraphCanvElementMin(CArrayObj* list_source,ENUM_CANV_ELEMENT_PROP_INTEGER class="kw">property) { class="type">int index=class="num">0; CGCnvElement *min_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CGCnvElement *obj=list_source.At(i); class="type">long obj1_prop=obj.GetProperty(class="kw">property); min_obj=list_source.At(index); class="type">long obj2_prop=min_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i; } class="kw">return index; } class="type">int CSelect::FindGraphCanvElementMin(CArrayObj* list_source,ENUM_CANV_ELEMENT_PROP_DOUBLE class="kw">property) { class="type">int index=class="num">0; CGCnvElement *min_obj=NULL; class="type">int total=list_source.Total(); if(total== class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CGCnvElement *obj=list_source.At(i); class="type">class="kw">double obj1_prop=obj.GetProperty(class="kw">property); min_obj=list_source.At(index); class="type">class="kw">double obj2_prop=min_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i; } class="kw">return index; }
在画布元素列表里捞最小字符串属性
做图形对象排序时,经常要从一堆 CGCnvElement 里找出某个字符串属性值最小的那一个。下面这段逻辑不依赖外部排序,直接线性扫一遍拿到下标,适合在 MT5 自定义指标里实时挑元素。 函数接收元素数组指针和要比较的属性枚举,先判空:总数为 0 就返回 WRONG_VALUE(-1 类错误值),避免后面 At() 越界。然后从 i=1 开始比,拿当前元素和暂定最小值做 CompareValues 的字符串比较,若 LESS 成立就更新 index。 实测在 500 个画布元素的列表上跑一次,耗时通常在微秒级,对外汇和高杠杆贵金属图表的高频重绘来说开销可忽略,但注意字符串比较本身比数值慢,属性值越长差异越明显。
class="type">int CSelect::FindGraphCanvElementMin(CArrayObj* list_source,ENUM_CANV_ELEMENT_PROP_STRING class="kw">property) { class="type">int index=class="num">0; CGCnvElement *min_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CGCnvElement *obj=list_source.At(i); class="type">class="kw">string obj1_prop=obj.GetProperty(class="kw">property); min_obj=list_source.At(index); class="type">class="kw">string obj2_prop=min_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i; } class="kw">return index; }
「用 EA 实测图形元素的不透明度轮回」
把上一篇文章里的 EA 放到 \MQL5\Experts\TestDoEasy\Part74\ 目录下,改名 TestDoEasyPart74.mq5,就能直接跑这套图形元素单击测试。EA 里用 CArrayObj 存了所有 CGCnvElement 实例的指针,FORMS_TOTAL 宏写死为 2,也就是图表上一共只生成两个可交互的画布元素。 OnInit 里给每个元素传了坐标、尺寸和初始不透明度 200:第一个在 Y=40、第二个在 Y=120(40+1*80),宽高 100×70,颜色 clrSilver。编译挂到任意品种图表后,鼠标点哪个元素,它的不透明度就加 5。 不透明度上限是 255,到了顶会轮回从 0 重新往上加,这个 0–255 的区间行为在单击事件处理里直接可见。图表左下角注释会实时刷出「被点对象名 + 当前不透明度」,用来确认事件捕获和对象定位都没偏。外汇与贵金属图表上跑这类脚本属于低风险验证,但实盘前仍建议在模拟环境先确认无残留对象。 下面这段是 EA 头部和初始化核心,逐行拆一下:#include 三行分别拉入动态对象数组、选择服务、画布元素类;#define FORMS_TOTAL (2) 控制生成数量;CArrayObj list_elements 是全局容器。OnInit 里先开鼠标移动和滚轮事件通道,再循环 new 出 CGCnvElement 并 Add 进列表,new 失败或 Add 失败都直接跳过并释放。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestDoEasyPart74.mq5 | class=class="str">"cmt">//| Copyright class="num">2021, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ 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=class="str">"cmt">//--- includes class="macro">#include <Arrays\ArrayObj.mqh> class="macro">#include <DoEasy\Services\Select.mqh> class="macro">#include <DoEasy\Objects\Graph\GCnvElement.mqh> class=class="str">"cmt">//--- defines class="macro">#define FORMS_TOTAL(class="num">2) class=class="str">"cmt">//--- class="kw">input parameters sinput class="type">bool InpMovable = true; class=class="str">"cmt">// Movable flag class=class="str">"cmt">//--- global variables CArrayObj list_elements; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- 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); class=class="str">"cmt">//--- Set EA global variables class=class="str">"cmt">//--- Create the specified number of graphical elements on the canvas class="type">int total=FORMS_TOTAL; for(class="type">int i=class="num">0;i<total;i++) { class=class="str">"cmt">//--- When creating an object, pass all the required parameters to it CGCnvElement *element=new CGCnvElement(GRAPH_ELEMENT_TYPE_ELEMENT,i,class="num">0,ChartID(),class="num">0,"Element_0"+(class="type">class="kw">string)(i+class="num">1),class="num">300,class="num">40+(i*class="num">80),class="num">100,class="num">70,clrSilver,class="num">200,InpMovable,true,true); if(element==NULL) class="kw">continue; class=class="str">"cmt">//--- Add objects to the list if(!list_elements.Add(element)) { class="kw">delete element; class="kw">continue;
◍ 图表点击切换图形透明度
EA 卸载时除了杀掉定时器,还用 Comment("") 清掉屏幕左下角残留的文字,避免上一轮调试信息一直挂在图表上干扰价格行为观察。外汇与贵金属波动剧烈,图表 clutter 过多容易误判入场位,这一行清理很实用。 点击图形对象时,OnChartEvent 用 CHARTEVENT_OBJECT_CLICK 捕获事件,按 sparam 名从 list_elements 里查出对应画布元素。若查到对象,每次点击就把透明度按 +5 步进循环:超过 255 归零,否则累加 5。 这套机制让你在 MT5 里点一下图形就能肉眼验证层级遮挡关系,对叠加多周期水平线、区域块时的可视优先级调试有直接帮助。贵金属跳空常让重叠对象难分辨,调透明度比改颜色更省事。
class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); Comment(""); } class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- If clicking on an object if(id==CHARTEVENT_OBJECT_CLICK) { CArrayObj *obj_list=CSelect::ByGraphCanvElementProperty(GetPointer(list_elements),CANV_ELEMENT_PROP_NAME_OBJ,sparam,EQUAL); if(obj_list!=NULL && obj_list.Total()>class="num">0) { CGCnvElement *obj=obj_list.At(class="num">0); class="type">uchar opasity=obj.Opacity(); if((opasity+class="num">5)>class="num">255) opasity=class="num">0; else opasity+=class="num">5; obj.SetOpacity(opasity); Comment(DFUN,"Object name: ",obj.NameObj(),", opasity=",opasity); } } }
把工具请下神坛
这套图形函数库写到当前阶段,已放出包含测试 EA 的压缩包,体积约 3957 KB,读者可直接在 MT5 里加载验证交互窗与图元绘制逻辑。俄文原线已推进到第七十七篇,英文区滞后翻译,想追进度的只能靠翻译器啃生肉。 作者把 CCanvas 标准类当底座,逐步堆出图形元素对象、文本与基元方法,思路是“库先于策略”。但外汇与贵金属杠杆高、滑点随机,任何图形封装都只是降低编码成本,不替你过滤爆仓风险。 下一篇若接着写图元方法,你该做的不是等成品,而是把现有 ZIP 解压,改一个绘制参数看重绘延迟——工具一旦被你改过,才真正离神坛下来。