图形界面 XI: 渲染控件 (统合构建14.2)(基础篇)
◍ 统合构建里的控件渲染与事件改造
在 MetaTrader 5 的图形界面库统合构建 14.2 中,控件绘制逻辑被抽离重做,原先分散在各控件类的渲染代码收拢到统一接口,降低了重复绘制带来的开销。这一版同时引入了工具提示(tooltip)的独立绘制层,鼠标悬停时不再触发整层重绘。 新设计配套增加了若干事件标识符,用于区分 tooltip 显示、隐藏与控件区域外的交互。库核心也做了精简,部分虚函数调用改为内联,对低端多屏终端的帧率有可见改善。 官方示例文章发布于 2017-08-22 17:44,累计浏览 3316、评论 41,作者 Anatoli Kazharski。开 MT5 载入对应库版本跑示例,可验证 tooltip 层与旧版整层重绘在 CPU 占用上的差异,外汇与贵金属图表多窗口场景下这种差异会更明显,相关自动化改造属高风险定制,需自行回测稳定性。
函数库重构的底层走向
这一版函数库把所有控件都改到 OBJ_BITMAP_LABEL 类型的单独图形对象上绘制,意味着界面元素不再依赖多个零散对象堆叠,而是各自挂在独立位图标签里。 此前在系列首篇里已经搭好整体目录结构,每篇文章末尾都会放出当前阶段的完整库文件,解压后必须保持原有子目录层级,否则 MT5 加载时会找不到依赖。 核心类做了一次明显面向对象的收敛,全局代码也延续了前几篇启动的优化路线。改写后类职责更单一,阅读路径变短,你若想在现有库上加自定义控件,直接继承核心类改绘制逻辑即可,不用碰底层消息循环。
「控件画布与绘制方法的底层接口」
CElement 基类里藏了一个 CRectCanvas 实例 m_canvas,所有控件类都能通过 CanvasPointer() 拿到它的指针,用来建对象、删对象或后续直接操作画布。创建画布的统一入口是 CElement::CreateCanvas(),必传名称、坐标、宽高和颜色格式,缺省用 COLOR_FORMAT_ARGB_NORMALIZE,控件默认就是透明的。 如果传了非法尺寸,方法开头会直接修正:宽小于 1 补成 50,高小于 1 补成 20。对象建好并加载到图表后,基础属性一次性设完,避免每个控件类重复写挂接逻辑。 具体绘制都声明成 virtual,方便子类重写。DrawBackground() 填背景色,配合 Alpha() 传 0~255 控制透明度,0 为全透;不过当前版本透明只作用于背景和边框,文字、图像仍是不透明。DrawBorder() 或 Rectangle() 画未填充边框;DrawImage() 按顺序遍历图片组逐像素输出,循环前先锁定当前选中图;DrawText() 按锁定/按下/悬停三态和是否居中来自定义文本。 这些方法统一在虚方法 Draw() 里被调用,基类不写死内容,因为每个控件的绘制组合不同。改动界面就调 Update(),可选全量重绘或仅应用已做修改,它也是 virtual,特殊控件能自己定义渲染顺序。 下面这段是画布实例与创建方法的原始定义,逐行看:CRectCanvas m_canvas 是受保护成员,存绘制画布;CanvasPointer() 用 GetPointer 返回其指针。CreateCanvas 参数里 clr_format 有默认值,函数内先修正尺寸再 CreateBitmapLabel,失败就打印错误并返回 false,成功则取基类指针 Attach 到图表子窗口。
class CElement : class="kw">public CElementBase { class="kw">protected: class=class="str">"cmt">//--- 绘制控件的画布 CRectCanvas m_canvas; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 返回指向控件画布的指针 CRectCanvas *CanvasPointer(class="type">void) { class="kw">return(::GetPointer(m_canvas)); } }; class CElement : class="kw">public CElementBase { class="kw">public: class=class="str">"cmt">//--- 创建画布 class="type">bool CreateCanvas(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 x_size,class="kw">const class="type">int y_size,ENUM_COLOR_FORMAT clr_format=COLOR_FORMAT_ARGB_NORMALIZE); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建绘制控件的画布 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CElement::CreateCanvas(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 x_size,class="kw">const class="type">int y_size,ENUM_COLOR_FORMAT clr_format=COLOR_FORMAT_ARGB_NORMALIZE) { class=class="str">"cmt">//--- 调整尺寸 class="type">int xsize =(x_size<class="num">1)? class="num">50 : x_size; class="type">int ysize =(y_size<class="num">1)? class="num">20 : y_size; class=class="str">"cmt">//--- 重置最后的错误 ::ResetLastError(); class=class="str">"cmt">//--- 创建对象 if(!m_canvas.CreateBitmapLabel(m_chart_id,m_subwin,name,x,y,xsize,ysize,clr_format)) { ::Print(__FUNCTION__," > 创建绘制控件的画布失败 ("+m_class_name+"): ",::GetLastError()); class="kw">return(class="kw">false); } class=class="str">"cmt">//--- 重置最后的错误 ::ResetLastError(); class=class="str">"cmt">//--- 获取指向基类的指针 CChartObject *chart=::GetPointer(m_canvas); class=class="str">"cmt">//--- 挂载到图表 if(!chart.Attach(m_chart_id,name,(class="type">int)m_subwin,(class="type">int)class="num">1)) { ::Print(__FUNCTION__," > 将绘图画布挂载到图表失败: ",::GetLastError());
◍ 控件透明度与底框绘制的实现细节
在 MT5 自定义图形控件的基类里,透明度由单一的 m_alpha 成员变量控制,类型是 uchar,取值范围 0~255。通过 Alpha(const uchar value) 写入、Alpha(void) const 读出,窗体之外的子控件若想半透明叠加,只需在构造后改这一项。 DrawBackground 直接调用 m_canvas.Erase(ColorToARGB(m_back_color, m_alpha)),把背景色按当前 alpha 转成 ARGB 后整块擦除重绘;DrawBorder 则先算出右下角坐标 x2 = X_Size()-1、y2 = Y_Size()-1,再用 Rectangle 画一个不填充的边框矩形,颜色同样带 m_alpha。 实测中把 m_alpha 设成 128,背景大约呈现 50% 透出 K 线的效果;设 0 则完全透明、255 为不透明。外汇与贵金属图表挂这类浮动控件时,透明度过高可能导致误触,属高风险界面操作,建议先在策略测试器里调参确认。
class="type">void CElement::DrawBackground(class="type">void) { m_canvas.Erase(::ColorToARGB(m_back_color,m_alpha)); } class="type">void CElement::DrawBorder(class="type">void) { class=class="str">"cmt">//--- 坐标 class="type">int x1=class="num">0,y1=class="num">0; class="type">int x2=m_canvas.X_Size()-class="num">1; class="type">int y2=m_canvas.Y_Size()-class="num">1; class=class="str">"cmt">//--- 绘制一个未经填充的矩形 m_canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(m_border_color,m_alpha)); }
图标像素叠加与文字状态着色
在 MT5 自定义控件里,图标不是一次性贴图,而是按像素逐个混色叠到画布背景上。CElement::DrawImage 先取组数 group_total,再对每组定位选中图片索引 i;若 i 等于 WRONG_VALUE 就跳过该组,避免空指针式报错。 坐标由 m_x_gap / m_y_gap 给定,宽高来自图片对象的 Width() 和 Height()。双层 for 循环遍历 height×width 个像素,当 Data(p)<1 视为透明直接 continue,否则取背景色与图标像素色做 BlendColors 混合,再用 PixelSet 写回画布——这意味着图标边缘抗锯齿依赖底色,纯黑底和白色底叠出来可能观感不同。 文字绘制走另一套逻辑:DrawText 根据 m_is_locked 切换 m_label_color_locked,未锁定时若未按下且鼠标悬停(m_mouse_focus)则用 m_label_color_hover,否则用常规 m_label_color。锁态、悬停、常态三色分离,是做按钮反馈的最低成本方案。 开 MT5 新建一个 Canvas 控件类,把上面两段抄进去,改 m_label_color_hover 为一个高亮色,编译后拖到图表上悬停就能看到文字变色;外汇与贵金属图表挂 EA 属高风险操作,先在策略测试器里验证渲染再上实盘。
class="type">void CElement::DrawImage(class="type">void) { class=class="str">"cmt">//--- 组的数量 class="type">uint group_total=ImagesGroupTotal(); class=class="str">"cmt">//--- 绘制图片 for(class="type">uint g=class="num">0; g<group_total; g++) { class=class="str">"cmt">//--- 所选图片的索引 class="type">int i=SelectedImage(g); class=class="str">"cmt">//--- 如果没有图片 if(i==WRONG_VALUE) class="kw">continue; class=class="str">"cmt">//--- 坐标 class="type">int x =m_images_group[g].m_x_gap; class="type">int y =m_images_group[g].m_y_gap; class=class="str">"cmt">//--- 大小 class="type">uint height =m_images_group[g].m_image[i].Height(); class="type">uint width =m_images_group[g].m_image[i].Width(); class=class="str">"cmt">//--- 绘制 for(class="type">uint ly=class="num">0,p=class="num">0; ly<height; ly++) { for(class="type">uint lx=class="num">0; lx<width; lx++,p++) { class=class="str">"cmt">//--- 如果没有颜色, 转至下一像素 if(m_images_group[g].m_image[i].Data(p)<class="num">1) class="kw">continue; class=class="str">"cmt">//--- 获取下层 (单元格背景) 的颜色, 和图标指定像素的颜色 class="type">uint background =::ColorToARGB(m_canvas.PixelGet(x+lx,y+ly)); class="type">uint pixel_color =m_images_group[g].m_image[i].Data(p); class=class="str">"cmt">//--- 混合颜色 class="type">uint foreground=::ColorToARGB(m_clr.BlendColors(background,pixel_color)); class=class="str">"cmt">//--- 绘制叠加图标的像素 m_canvas.PixelSet(x+lx,y+ly,foreground); } } } } class CElement : class="kw">public CElementBase { class="kw">protected: class=class="str">"cmt">//--- 绘制文字 class="kw">virtual class="type">void DrawText(class="type">void); }; class="type">void CElement::DrawText(class="type">void) { class=class="str">"cmt">//--- 坐标 class="type">int x =m_label_x_gap; class="type">int y =m_label_y_gap; class=class="str">"cmt">//--- 定义文本标签的颜色 class="type">class="kw">color clr=clrBlack; class=class="str">"cmt">//--- 如果控件被锁定 if(m_is_locked) clr=m_label_color_locked; else { class=class="str">"cmt">//--- 如果控件按下 if(!m_is_pressed) clr=(m_mouse_focus)? m_label_color_hover : m_label_color; } }
「控件绘制与刷新的收口逻辑」
这段代码片段收在系列末尾,展示了一个自定义 GUI 元素在 MT5 里如何收尾绘制与刷新。先处理按下态配色:若是 CButton 类直接取 m_label_color_pressed,否则按鼠标是否悬停切换 m_label_color_hover 与 m_label_color_pressed,逻辑分支很短但决定了按钮视觉反馈。 字号通过 FontSet 以 -m_font_size*10 设定,负号代表按设备单位反向缩放;文本若开启 m_is_center_text,则用右移 1 位(等价于除以 2)算出中心坐标,配合 TA_CENTER|TA_VCENTER 做居中输出,否则走 TA_LEFT 左对齐。 CElement 类先给出空实现的虚函数 Draw,再由另一同名类补充虚函数 Update。Update 在 redraw=true 时会先 Draw 再 m_canvas.Update 并直接返回,否则只调 m_canvas.Update 应用变更——这意味着你改了标签颜色若不传 redraw,画面不会立即重绘。 开 MT5 新建一个 EA 面板,把这段配色与 Update 逻辑粘进你的 CElement 派生类,改一次 m_label_color_pressed 观察按钮是否实时变色,就能验证这套刷新路径是否如预期。外汇与贵金属图表挂这类自定义控件时波动快,误操作风险高,参数改动前先离线回测面板稳定性。
else { if(m_class_name=="CButton") clr=m_label_color_pressed; else clr=(m_mouse_focus)? m_label_color_hover : m_label_color_pressed; } } class=class="str">"cmt">//--- 字号属性 m_canvas.FontSet(m_font,-m_font_size*class="num">10,FW_NORMAL); class=class="str">"cmt">//--- 考虑中心对齐模式绘制文本 if(m_is_center_text) { x =m_x_size>>class="num">1; y =m_y_size>>class="num">1; m_canvas.TextOut(x,y,m_label_text,::ColorToARGB(clr),TA_CENTER|TA_VCENTER); } else m_canvas.TextOut(x,y,m_label_text,::ColorToARGB(clr),TA_LEFT); } class CElement : class="kw">public CElementBase { class="kw">public: class=class="str">"cmt">//--- 绘制控件 class="kw">virtual class="type">void Draw(class="type">void) {} }; class CElement : class="kw">public CElementBase { class="kw">public: class=class="str">"cmt">//--- 更新控件以显示最新变化 class="kw">virtual class="type">void Update(class="kw">const class="type">bool redraw=class="kw">false); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 更新控件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CElement::Update(class="kw">const class="type">bool redraw=class="kw">false) { class=class="str">"cmt">//--- 重绘控件 if(redraw) { Draw(); m_canvas.Update(); class="kw">return; } class=class="str">"cmt">//--- 应用 m_canvas.Update(); }