图形界面 X 渲染表格新功能:从静态画板到可交互控件的基础铺垫(基础篇)
(1/3)·老版 CTable 已达手动拖拽列宽的极限,本篇先讲清渲染表格的斑马样式与行选择底层逻辑
◍ MT5 表格控件的新渲染能力
MetaTrader 5 在集成编译版本 9 中,为图形界面 X 的表格组件补上了多项原生渲染能力,主要覆盖斑马纹格式化、行选中状态切换、列标题固定以及按列宽自动截断字符串。这类改动不依赖外部库,直接降低自定义面板开发成本。 官方示例发布于 2017 年 4 月 5 日 13:17,截至统计节点获得 3 372 次查看与 24 条互动,说明当时交易者对轻量 GUI 方案需求明确。 实际价值在于:你可以在 EA 的面板里用标准控件画出可读的报价/持仓表,而不必手绘像素。外汇与贵金属波动快、跳价频繁,这类表格在高杠杆环境下仅作信息展示,不构成任何方向建议,请自行评估风险。
「为什么旧表格类走到头了」
在 MT5 标准库的早期封装里,CTable 曾是最复杂的表格类型,它由一堆 OBJ_EDIT 编辑框拼出来。但实测到后期就卡住了:想手动拖标题边框调列宽基本做不到,单个图形对象的可见区域也没法独立管,扩展成本越来越高。 所以现阶段更划算的做法是转向 CCanvasTable 这类基于画布的表格。旧版渲染表格(见图形界面 VII、X 相关章节)跑出来长这样:截图里它就是个静态数据格子,能设单元格对齐,除了滚动条和窗体自适应外,几乎没有交互。 这次更新准备给渲染表格补这几样硬功能:斑马纹格式化、点行选中再点弃选、列标题悬浮和点击变色、文本超出时自动撑列宽、拖边框改列宽。外汇和贵金属图表挂这类界面时波动快,先在小周期回测里验证交互开销,高风险品种别直接上实盘。
给 MT5 表格加上斑马纹路
在 MT5 自定义面板里,当 CTable 塞进几十上百个单元格时,肉眼追踪某一行会变得吃力。CCanvasTable 提供的斑马样式就是为这个场景准备的:隔行换色,让视线能顺着明暗带走。 启用只需调用 IsZebraFormatRows(),传进去的第二个 color 参数就是交替色,普通单元格仍用原有的 m_cell_color。从代码看,未启用时(参数为 clrNONE)直接用 Erase 整块填色;启用后进入循环,逐行算 y 坐标并用 FillRectangle 交替着色。 循环里 r%2!=0 决定该行用斑马色还是基色,这意味着第 0、2、4… 行是基色,第 1、3、5… 行是你设的交替色。行高由 m_cell_y_size 控制,y1 还减了 r 做 1 像素行间距修正——这个细节不处理,密集表格会糊成一片。 外汇与贵金属图表挂这类面板属高风险环境下的辅助显示,斑马纹只解决可读性,不预示任何价格方向。开 MT5 把下面类片段塞进你的 CCanvasTable 派生类,改两行颜色就能直接看效果。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建渲染表格的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CCanvasTable : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 格式化斑马样式模式 class="type">class="kw">color m_is_zebra_format_rows; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 格式化斑马样式 class="type">void IsZebraFormatRows(class="kw">const class="type">class="kw">color clr) { m_is_zebra_format_rows=clr; } }; class CCanvasTable : class="kw">public CElement { class="kw">public: class=class="str">"cmt">//--- 绘制表格行的背景 class="type">void DrawRows(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 绘制表格行的背景 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CCanvasTable::DrawRows(class="type">void) { class=class="str">"cmt">//--- 如果禁用格式化斑马样式模式 if(m_is_zebra_format_rows==clrNONE) { class=class="str">"cmt">//--- 用一种颜色填充画板 m_table.Erase(::ColorToARGB(m_cell_color)); class="kw">return; } class=class="str">"cmt">//--- 标题坐标 class="type">int x1=class="num">0,x2=m_table_x_size; class="type">int y1=class="num">0,y2=class="num">0; class=class="str">"cmt">//--- 格式化斑马样式 for(class="type">int r=class="num">0; r<m_rows_total; r++) { class=class="str">"cmt">//--- 计算坐标 y1=(r*m_cell_y_size)-r; y2=y1+m_cell_y_size; class=class="str">"cmt">//--- 行颜色 class="type">uint clr=::ColorToARGB((r%class="num">2!=class="num">0)? m_is_zebra_format_rows : m_cell_color); class=class="str">"cmt">//--- 绘制行背景 m_table.FillRectangle(x1,y1,x2,y2,clr); } }
◍ 给画布表格加行选中交互
在 MT5 自定义画布表格里做行选中,不能只靠画背景,得额外存两组状态:选中行的背景色与文本色,以及该行的索引和首列文本。先调用 CCanvasTable::SelectableRow() 把可选模式打开,否则后续点击逻辑直接跳过。 双击行触发选择,事件入口是 CCanvasTable::OnClickTable(),由 CHARTEVENT_OBJECT_CLICK 驱动。方法开头有三道拦截:可选模式关闭、滚动条激活、点击落在表格外——任一命中就 return,不进坐标计算。 通过拦截后,取画板边缘偏移与鼠标 Y 坐标,在循环里比对出被点中的行。若该行已选则取消,否则把索引和首列文本写进 m_selected_item / m_selected_item_text,重绘后发出 ON_CLICK_LIST_ITEM 消息,带控件 ID、行索引和行文本,方便上层 EA 接数据。 下面这段类声明和绘制方法是核心骨架,双击验证时重点看 y_offset 的算法:用选中行号乘单元格高度再减行号,刚好跳过行间 1 像素分隔。
<span class="keyword">class</span> CCanvasTable : <span class="keyword">class="kw">public</span> CElement { <span class="keyword">class="kw">private</span>: <span class="comment">class=class="str">"cmt">//--- 颜色用于 (class="num">1) 背景 (class="num">2) 所选行文本</span> <span class="keyword">class="type">class="kw">color</span> m_selected_row_color; <span class="keyword">class="type">class="kw">color</span> m_selected_row_text_color; <span class="comment">class=class="str">"cmt">//--- (class="num">1) 索引 (class="num">2) 所选行文本</span> <span class="keyword">class="type">int</span> m_selected_item; <span class="keyword">class="type">class="kw">string</span> m_selected_item_text; <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="kw">public</span>: <span class="comment">class=class="str">"cmt">//--- 返回 (class="num">1) 索引 (class="num">2) 表格中所选行文本</span> <span class="keyword">class="type">int</span> SelectedItem(<span class="keyword">class="type">void</span>) <span class="keyword">class="kw">const</span> { <span class="keyword">class="kw">return</span>(m_selected_item); } <span class="keyword">class="type">class="kw">string</span> SelectedItemText(<span class="keyword">class="type">void</span>) <span class="keyword">class="kw">const</span> { <span class="keyword">class="kw">return</span>(m_selected_item_text); } <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="kw">private</span>: <span class="comment">class=class="str">"cmt">//--- 绘制表格行的背景</span> <span class="keyword">class="type">void</span> DrawRows(<span class="keyword">class="type">void</span>); }; <span class="keyword">class</span> CCanvasTable : <span class="keyword">class="kw">public</span> CElement { <span class="keyword">class="kw">private</span>: <span class="comment">class=class="str">"cmt">//--- 可选行模式</span> <span class="keyword">class="type">bool</span> m_selectable_row; <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="kw">public</span>: <span class="comment">class=class="str">"cmt">//--- 行选择</span> <span class="keyword">class="type">void</span> SelectableRow(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">bool</span> flag) { m_selectable_row=flag; } }; <span class="keyword">class</span> CCanvasTable : <span class="keyword">class="kw">public</span> CElement { <span class="keyword">class="kw">private</span>: <span class="comment">class=class="str">"cmt">//--- 绘制所选行</span> <span class="keyword">class="type">void</span> DrawSelectedRow(<span class="keyword">class="type">void</span>); }; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| 绘制所选行 |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> CCanvasTable::DrawSelectedRow(<span class="keyword">class="type">void</span>) { <span class="comment">class=class="str">"cmt">//--- 设置初始坐标以便检查条件</span> <span class="keyword">class="type">int</span> y_offset=(m_selected_item*m_cell_y_size)-m_selected_item; <span class="comment">class=class="str">"cmt">//--- 坐标</span> <span class="keyword">class="type">int</span> x1=<span class="number">class="num">0</span>,x2=<span class="number">class="num">0</span>,y1=<span class="number">class="num">0</span>,y2=<span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">//---</span> x1=<span class="number">class="num">0</span>; y1=y_offset; x2=m_table_x_size; y2=y_offset+m_cell_y_size-<span class="number">class="num">1</span>; <span class="comment">class=class="str">"cmt">//--- 绘制填充矩形</span> <span style="background-class="type">class="kw">color:rgb(class="num">255, class="num">242, class="num">153);">m_table.FillRectangle(x1,y1,x2,y2,::<span class="functions">ColorToARGB</span>(m_selected_row_color));</span> } <span class="keyword">class</span> CCanvasTable : <span class="keyword">class="kw">public</span> CElement { <span class="keyword">class="kw">private</span>: <span class="comment">class=class="str">"cmt">//--- 返回单元格文本的颜色</span> <span class="keyword">class="type">uint</span> TextColor(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> row_index); }; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
「表格行点击与选中状态的底层逻辑」
在 MT5 自定义表格控件里,行选中不是靠按钮,而是靠鼠标坐标反推。OnClickTable 先卡三道门槛:未开启行选模式、垂直或水平滚动条正被拖拽、点击对象名和表格名对不上,任一命中直接 return false,避免误触。 过了门槛才取表格的 X/Y 偏移,用 m_mouse.Y() 减表基准坐标再加偏移,得到鼠标在表内的纵向位置 y。循环 0 到 m_rows_total-1,每行高 m_cell_y_size 且行间隙占 1 像素,所以 y_offset=(r*m_cell_y_size)-r 是该行顶部;y 落在此区间才认作点中。 点中已经选中的行会取消选择,m_selected_item 置为 WRONG_VALUE、文本清空;否则存下行号与第 0 列文本。最后重绘并发 EventChartCustom,把选中行号通过自定义事件甩给图表其他模块。 TextColor 方法只做一件事:选中的行用 m_selected_row_text_color,其余用 m_cell_text_color,经 ColorToARGB 转 32 位色值返回。你在 EA 里接 ON_CLICK_LIST_ITEM 事件,就能拿到具体点了哪一行,外汇与贵金属图表交互高风险,参数改错可能点哪行都无响应。
class="type">uint CCanvasTable::TextColor(class="kw">const class="type">int row_index) { class="type">uint clr=::ColorToARGB((row_index==m_selected_item)? m_selected_row_text_color : m_cell_text_color); class=class="str">"cmt">//--- 返回标题颜色 class="kw">return(clr); } class CCanvasTable : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 在元素上点按处理 class="type">bool OnClickTable(class="kw">const class="type">class="kw">string clicked_object); }; class="type">bool CCanvasTable::OnClickTable(class="kw">const class="type">class="kw">string clicked_object) { class=class="str">"cmt">//--- 如果禁用选择行模式, 离开 if(!m_selectable_row) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 如果滚动条激活, 离开 if(m_scrollv.ScrollState() || m_scrollh.ScrollState()) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 如果对象名不同, 离开 if(m_table.Name()!=clicked_object) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 获取 X 和 Y 轴的偏移量 class="type">int xoffset=(class="type">int)m_table.GetInteger(OBJPROP_XOFFSET); class="type">int yoffset=(class="type">int)m_table.GetInteger(OBJPROP_YOFFSET); class=class="str">"cmt">//--- 确定鼠标光标下方的文本编辑框坐标 class="type">int y=m_mouse.Y()-m_table.Y()+yoffset; class=class="str">"cmt">//--- 确定点击的行 for(class="type">int r=class="num">0; r<m_rows_total; r++) { class=class="str">"cmt">//--- 设置初始坐标以便检查条件 class="type">int y_offset=(r*m_cell_y_size)-r; class=class="str">"cmt">//--- 沿 Y 轴检查状态 class="type">bool y_pos_check=(y>=y_offset && y<y_offset+m_cell_y_size); class=class="str">"cmt">//--- 如果点击并非此行, 则转至下一个 if(!y_pos_check) class="kw">continue; class=class="str">"cmt">//--- 如果点击已选行, 取消选择 if(r==m_selected_item) { m_selected_item =WRONG_VALUE; m_selected_item_text =""; class="kw">break; } class=class="str">"cmt">//--- 保存行索引 m_selected_item =r; m_selected_item_text =m_vcolumns[class="num">0].m_vrows[r]; class="kw">break; } class=class="str">"cmt">//--- 表格重绘 DrawTable(); class=class="str">"cmt">//--- 发送有关消息 ::EventChartCustom(m_chart_id,ON_CLICK_LIST_ITEM,CElementBase::Id(),m_selected_item,m_selected_item_text); class="kw">return(true); }
给画布表格补上可交互列头
在 MT5 里用 CCanvasTable 自绘表格时,没标题的表就是一片空白。类里额外挂了一个 CRectCanvas 实例 m_headers,专门在独立画板上画列头,创建逻辑和建表几乎一致,只是预设的尺寸和坐标不同。 列头在表创建前就能配置四项属性:显示开关、高度、三种状态背景色(普通/悬浮/按下)、文本色。设置标题名需要配套一个长度等于列数的数组,它在 TableSize() 里和列数同步分配。文本对齐复用 TextAlign(),X 轴跟列头一致,Y 轴列头固定 TA_VCENTER,单元格则从顶边用 TA_TOP 偏移。 想模拟系统工具箱那种边框改列宽的手感,要在 ENUM_MOUSE_POINTER 里加沿 X/Y 缩放的指针 ID,并在 CPointer 中注册。为了省 CPU,只有鼠标跨过相邻标题边界交错时才重绘——HeaderColorCurrent() 按光标位置和左键状态取色,DrawHeaders() 先填整板背景,再判断焦点列并记下索引,之后 DrawHeadersGrid() 画外框和分割线,DrawHeadersText() 循环算坐标和对齐,用 CorrectingText() 随列宽收尾。 CheckHeaderFocus() 在标题禁用或已开始改宽时直接返回,否则循环找新焦点,边界交错才重置旧索引并中断。ChangeHeadersColor() 同样靠交错触发重绘。CheckColumnResizeFocus() 负责边框焦点和光标显隐:未改宽时悬在边框上就亮指针,否则隐藏。外汇和贵金属图表挂这类自绘控件时,注意高频重绘可能拖慢终端,属高风险环境下的性能取舍。
class CCanvasTable : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 创建表格的对象 CRectCanvas m_headers; class=class="str">"cmt">//--- class="kw">private: class="type">bool CreateHeaders(class="type">void); }; class CCanvasTable : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 表头的显示模式 class="type">bool m_show_headers; class=class="str">"cmt">//--- 标题的大小 (高度) class="type">int m_header_y_size; class=class="str">"cmt">//--- 不同状态下的标题 (背景) 颜色 class="type">class="kw">color m_headers_color; class="type">class="kw">color m_headers_color_hover; class="type">class="kw">color m_headers_color_pressed; class=class="str">"cmt">//--- 标题文本颜色 class="type">class="kw">color m_headers_text_color; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- (class="num">1) 标题显示模式, (class="num">2) 标题高度 class="type">void ShowHeaders(class="kw">const class="type">bool flag) { m_show_headers=flag; } class="type">void HeaderYSize(class="kw">const class="type">int y_size) { m_header_y_size=y_size; } class=class="str">"cmt">//--- (class="num">1) 背景 (class="num">2) 标题文本颜色 class="type">void HeadersColor(class="kw">const class="type">class="kw">color clr) { m_headers_color=clr; } class="type">void HeadersColorHover(class="kw">const class="type">class="kw">color clr) { m_headers_color_hover=clr; } class="type">void HeadersColorPressed(class="kw">const class="type">class="kw">color clr) { m_headers_color_pressed=clr; }