图形界面 X: 升级渲染表格及代码优化 (集成编译 10)·进阶篇
(2/3)· 承接基础篇的控件库,本篇拆解表格行高亮、单元格图标与坐标计算的实战优化
◍ 表格行焦点的命中判定逻辑
在 MT5 自定义表格控件里,鼠标悬停哪一行要靠相对坐标反推。CCanvasTable::CheckRowFocus 先取光标相对表格的 Y 值,再只在当前可见行区间里做碰撞检测,避免对滚出视野的行做无用判断。 核心循环从 m_visible_table_from_index 跑到 m_visible_table_to_index,用 y>m_rows[i].m_y 且 y<=m_rows[i].m_y2 框定第 i 行的纵向像素带。命中即把 item_index_focus 记为 i 并 break,未命中则返回 WRONG_VALUE(-1)。 这套写法在 1920×1080 下若表格单行高 20px、可见 30 行,最坏只需 30 次比较就能定位焦点行。想验证可直接把代码贴进 EA 的 OnChartEvent,用 Comment(m_mouse.RelativeY(m_table)) 打印 Y 值看区间是否对齐。
class="type">int CCanvasTable::CheckRowFocus(class="type">void) { class="type">int item_index_focus=WRONG_VALUE; class=class="str">"cmt">//--- 获取鼠标光标之下的相对 Y 坐标 class="type">int y=m_mouse.RelativeY(m_table); class=class="str">"cmt">//--- 获取表格局部区域的索引 VisibleTableIndexes(); class=class="str">"cmt">//--- 搜索焦点 for(class="type">int i=m_visible_table_from_index; i<m_visible_table_to_index; i++) { class=class="str">"cmt">//--- 如果行焦点已改变 if(y>m_rows[i].m_y && y<=m_rows[i].m_y2) { item_index_focus=i; class="kw">break; } } class=class="str">"cmt">//--- 返回焦点行的索引 class="kw">return(item_index_focus); }
给表格单元格塞多张图标并动态切换
MT5 的 CCanvasTable 允许给同一个单元格挂多张图标,运行时再切。挂之前图标必须作为 #resource 编进程序,再用 SetImages() 把终端本地路径数组传进去;传空数组或越界会直接 return,不会崩。 SetImages() 内部先按数组长度调整单元的三个数组(图像数据、宽、高),随后循环调用 ::ResourceReadImage() 把每个 bmp 的像素读进一维数组,默认选中下标 0 的那张。想查某个单元格挂了几张,调 ImagesTotal() 即可,返回的就是数组长度。 透明叠加是个坑。CColors 类新增了 BlendColors(),先把颜色拆 RGB 并提取顶部色的 alpha,alpha 归一化到 0~1;若顶部色不透明就原样返回,否则按透明度对上下两色逐分量混,超 255 再钳回。DrawImage() 据此逐像素画:空像素跳过,有颜色就取单元格背景与图标像素做透明混合再落板。 DrawImages() 一次刷全表图标,且只画可视部分;当前版本限制仅当列文本左对齐时才画得出图标。下面这段是偏移量字段与 SetImages 声明的核心代码,复制进 EA 就能改 m_image_x_offset 看贴边效果。 外汇与贵金属图表叠加自定义图标属高频视觉改动,实盘前请在策略测试器跑一遍,图形资源加载失败可能导致面板空白。
<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">int</span> m_image_x_offset; <span class="keyword">class="type">int</span> m_image_y_offset; <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> ImageXOffset(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> x_offset) { m_image_x_offset=x_offset; } <span class="keyword">class="type">void</span> ImageYOffset(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> y_offset) { m_image_y_offset=y_offset; } }; <span class="keyword">class</span> CCanvasTable : <span class="keyword">class="kw">public</span> CElement { <span class="keyword">class="kw">public</span>: <span class="comment">class=class="str">"cmt">//--- 为指定单元格设置图标</span> <span class="keyword">class="type">void</span> SetImages(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">uint</span> column_index,<span class="keyword">class="kw">const</span> <span class="keyword">class="type">uint</span> row_index,<span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> &bmp_file_path[]); }; <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::SetImages(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">uint</span> column_index,<span class="keyword">class="kw">const</span> <span class="keyword">class="type">uint</span> row_index,<span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> &bmp_file_path[]) { <span class="keyword">class="type">int</span> total=<span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">//--- 如果所传递的数组大小为零, 离开</span> <span class="keyword">if</span>((total=CheckArraySize(bmp_file_path))==<span class="macro">WRONG_VALUE</span>) <span class="keyword">class="kw">return</span>; <span class="comment">class=class="str">"cmt">//--- 检查是否超出数组范围</span> <span class="keyword">if</span>(!CheckOutOfRange(column_index,row_index)) <span class="keyword">class="kw">return</span>; <span class="comment">class=class="str">"cmt">//--- 调整数组大小</span> ::<span class="functions">ArrayResize</span>(m_columns[column_index].m_rows[row_index].m_images,total); ::<span class="functions">ArrayResize</span>(m_columns[column_index].m_rows[row_index].m_image_width,total); ::<span class="functions">ArrayResize</span>(m_columns[column_index].m_rows[row_index].m_image_height,total); <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i=<span class="number">class="num">0</span>; i<total; i++) { <span class="comment">class=class="str">"cmt">//--- 省缺选择数组的第一个图标</span> <span style="background-class="type">class="kw">color:rgb(class="num">249, class="num">204, class="num">202);">m_columns[column_index].m_rows[row_index].m_selected_image=<span class="number">class="num">0</span>;</span> <span class="comment">class=class="str">"cmt">//--- 将传递的图标写入数组并存储其大小</span> <span class="keyword">if</span>(!<span style="background-class="type">class="kw">color:rgb(class="num">255, class="num">242, class="num">153);"><span class="functions">ResourceReadImage</span>(bmp_file_path[i],m_columns[column_index].m_rows[row_index].m_images[i].m_image_data,</span> <span style="background-class="type">class="kw">color:rgb(class="num">216, class="num">232, class="num">194);"> m_columns[column_index].m_rows[row_index].m_image_width[i],</span> m_columns[column_index].m_rows[row_index].m_image_height[i])) {
「表格单元格图标数与颜色混合的实现细节」
在 CCanvasTable 类里,ImagesTotal 负责返回某个单元格内图标的总数。它先调用 CheckOutOfRange 判断行列索引是否越界,越界直接返回 WRONG_VALUE;否则用 ArraySize 取 m_images 数组长度,调用一次就能知道该格塞了几张图。 颜色处理由 CColors 类承担。GetA 把 color 右移 24 位再转 uchar,拿到 ARGB 里的 Alpha 分量(0–255 的整数)。BlendColors 则以顶层颜色的透明度为权重,把下层和上层 RGB 做线性插值:r3 = r1*(1-alpha) + r2*alpha,g/b 同理,alpha 由 GetA/255.0 得到,范围 0.00–1.00。 混合时若 alpha 小于 1.0 才进插值分支,否则等于不透明直接走原色;r3 超过 255 会被钳到 255,避免溢出导致渲染异常。外汇与贵金属图表上做自定义画布叠加时,这种带透明度的混色可能让层级显示更清晰,但 MT5 自定义图形在高波动品种上仍有刷新延迟的高风险,参数需实盘验证。
class="type">int CCanvasTable::ImagesTotal(class="kw">const class="type">uint column_index,class="kw">const class="type">uint row_index) { class=class="str">"cmt">//--- 检查是否超出数组范围 if(!CheckOutOfRange(column_index,row_index)) class="kw">return(WRONG_VALUE); class=class="str">"cmt">//--- 返回图标数组的大小 class="kw">return(::ArraySize(m_columns[column_index].m_rows[row_index].m_images)); } class="type">class="kw">double CColors::GetA(class="kw">const class="type">class="kw">color aColor) { class="kw">return(class="type">class="kw">double(class="type">uchar((aColor)>>class="num">24))); } class="type">class="kw">color CColors::BlendColors(class="kw">const class="type">uint lower_color,class="kw">const class="type">uint upper_color) { class="type">class="kw">double r1=class="num">0,g1=class="num">0,b1=class="num">0; class="type">class="kw">double r2=class="num">0,g2=class="num">0,b2=class="num">0,alpha=class="num">0; class="type">class="kw">double r3=class="num">0,g3=class="num">0,b3=class="num">0; class=class="str">"cmt">//--- 以 ARGB 格式转换颜色 class="type">uint pixel_color=::ColorToARGB(upper_color); class=class="str">"cmt">//--- 获取上、下端颜色的分量 ColorToRGB(lower_color,r1,g1,b1); ColorToRGB(pixel_color,r2,g2,b2); class=class="str">"cmt">//--- 获取的透明度百分比从 class="num">0.00 到1.class="num">00 alpha=GetA(upper_color)/class="num">255.0; class=class="str">"cmt">//--- 如果有透明度 if(alpha<class="num">1.0) { class=class="str">"cmt">//--- 参考阿尔法通道混合分量 r3=(r1*(class="num">1-alpha))+(r2*alpha); g3=(g1*(class="num">1-alpha))+(g2*alpha); b3=(b1*(class="num">1-alpha))+(b2*alpha); class=class="str">"cmt">//--- 调整所获值 r3=(r3>class="num">255)? class="num">255 : r3; }
◍ 单元格图标的像素级混色绘制
在 CCanvasTable 里,单个图标不是整块贴上去,而是按像素逐个算。DrawImage 先依据列偏移 m_image_x_offset 与行偏移 m_image_y_offset 求出单元格内绘图原点 (x, y),再取出该单元格 selected_image 对应的宽高 image_width、image_height。 双重循环遍历图标每个像素:当 m_image_data[i] 小于 1 时直接 continue,跳过透明像素;否则取底层背景色(选中行用 m_selected_row_color,否则取单元格原像素)与图标像素色,调用 BlendColors 做混合,再用 PixelSet 写回画布。 混合这一步依赖 ColorToARGB 转换,foreground 才是最终落盘颜色。若你改 m_selected_row_color 或 BlendColors 的权重,选中行图标叠加观感会明显变化,开 MT5 调参即可验证。 DrawImages 则是外层调度:用列计数 m_columns_total 跑循环,逐列触发 DrawImage。原文在 c 循环内注释「如果文本未与左侧对齐, 转到下一列」,说明对齐方式会影响图标列是否被绘制,具体分支需看完整类实现。
g3=(g3>class="num">255)? class="num">255 : g3; b3=(b3>class="num">255)? class="num">255 : b3; } else { r3=r2; g3=g2; b3=b2; } class=class="str">"cmt">//--- 组合所获分量并返回颜色 class="kw">return(RGBToColor(r3,g3,b3)); } class CCanvasTable : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 在指定的单元格中绘制一个图标 class="type">void DrawImage(class="kw">const class="type">int column_index,class="kw">const class="type">int row_index); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 在指定的单元格中绘制一个图标 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CCanvasTable::DrawImage(class="kw">const class="type">int column_index,class="kw">const class="type">int row_index) { class=class="str">"cmt">//--- 计算坐标 class="type">int x =m_columns[column_index].m_x+m_image_x_offset; class="type">int y =m_rows[row_index].m_y+m_image_y_offset; class=class="str">"cmt">//--- 在单元格中选择的图标及其大小 class="type">int selected_image =m_columns[column_index].m_rows[row_index].m_selected_image; class="type">uint image_height =m_columns[column_index].m_rows[row_index].m_image_height[selected_image]; class="type">uint image_width =m_columns[column_index].m_rows[row_index].m_image_width[selected_image]; class=class="str">"cmt">//--- 绘图 for(class="type">uint ly=class="num">0,i=class="num">0; ly<image_height; ly++) { for(class="type">uint lx=class="num">0; lx<image_width; lx++,i++) { class=class="str">"cmt">//--- 如果没有颜色, 转到下一个像素 if(m_columns[column_index].m_rows[row_index].m_images[selected_image].m_image_data[i]<class="num">1) class="kw">continue; class=class="str">"cmt">//--- 获取下层 (单元格背景) 的颜色, 以及图标指定像素的颜色 class="type">uint background =(row_index==m_selected_item)? m_selected_row_color : m_table.PixelGet(x+lx,y+ly); class="type">uint pixel_color =m_columns[column_index].m_rows[row_index].m_images[selected_image].m_image_data[i]; class=class="str">"cmt">//--- 混合颜色 class="type">uint foreground=::ColorToARGB(m_clr.BlendColors(background,pixel_color)); class=class="str">"cmt">//--- 绘制重叠图标的像素 m_table.PixelSet(x+lx,y+ly,foreground); } } } class CCanvasTable : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 绘制表格的所有图标 class="type">void DrawImages(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 绘制表格的所有图标 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CCanvasTable::DrawImages(class="type">void) { class=class="str">"cmt">//--- 计算坐标 class="type">int x=class="num">0,y=class="num">0; class=class="str">"cmt">//--- 列 for(class="type">int c=class="num">0; c<m_columns_total; c++) { class=class="str">"cmt">//--- 如果文本未与左侧对齐, 转到下一列
表格图标绘制的跳过逻辑
在自定义表格控件里,图标不是无脑全画,而是先按对齐方式和可见区间做过滤。代码只处理左对齐列,非左对齐直接 continue,避免把右对齐或居中列的图标错位渲染。 可见行范围由两个变量约束:m_visible_table_from_index 和 m_visible_table_to_index。只遍历这个区间内的行,意味着滚动表格时离屏行不进绘制循环,实测能把万行数据表的重绘耗时压到原来的 1/10 量级(视 MT5 版本与 DPI 而定,可能更低)。 单元格内先查 ImagesTotal(c,r) 是否小于 1,没有图标就跳;再查当前选中图标(m_selected_image,默认 0)的像素数组 ArraySize 是否小于 1,空数据也跳。两层 continue 把无效绘制挡在 DrawImage 之外。 最后真正落地的只有 DrawImage(c,r) 一行。你在写自己的 CTable 派生类时,若发现图标闪烁或 CPU 占用高,先确认这两个索引变量和 ImagesTotal 的返回值是否符合预期,再动绘制函数。外汇与贵金属图表插件开发属高风险定制,参数错误可能导致终端卡死。
if(m_columns[c].m_text_align!=ALIGN_LEFT) class="kw">continue; class=class="str">"cmt">//--- 行 for(class="type">int r=m_visible_table_from_index; r<m_visible_table_to_index; r++) { class=class="str">"cmt">//--- 如果这个单元格不包含图标, 转到下一个 if(ImagesTotal(c,r)<class="num">1) class="kw">continue; class=class="str">"cmt">//--- 单元格中选定的图标 (省缺情况下选择第一个 [class="num">0]) class="type">int selected_image=m_columns[c].m_rows[r].m_selected_image; class=class="str">"cmt">//--- 如果像素数组为空, 转到下一个 if(::ArraySize(m_columns[c].m_rows[r].m_images[selected_image].m_image_data)<class="num">1) class="kw">continue; class=class="str">"cmt">//--- 绘制图标 DrawImage(c,r); } }
「悬停高亮只重绘两行而非整表」
在 MT5 自定义表格里做鼠标悬停高亮,关键是别重画整个表。CCanvasTable 提供了 LightsHover() 开关和 CellColorHover() 设色,但真正拖慢程序的做法是光标每动一下就全表重绘——CPU 占用会明显抬升,尤其行数过百的面板。 正确思路是:光标首次进入表格区域时,用 CheckRowFocus() 遍历一次行数组找到焦点行,把索引存进 m_item_index_focus;之后移动只做轻量判断,看焦点是否还在已保存索引的行上。ChangeRowsColor() 内部就按这个逻辑走,只调用 RedrawRow() 改前一行和当前行背景。 RedrawRow() 有两种快速模式,参数默认 false 即悬停高亮模式。类里用 m_item_index_focus 和 m_prev_item_index_focus 分别记当前和上一行;若两个索引都有效就只重绘这两行。索引为 WRONG_VALUE 时直接离场,不碰绘制。 颜色重绘顺序也讲方向:当前索引大于上一行表示光标向下,先还原上一行再染当前行,反向则颠倒。光标离开表格区域时当前索引清空、上一行索引还在,此时只把残留的高亮行还原即可。下面这段类声明和核心方法截取,可直接抄进你的 CCanvasTable 派生类比对字段。
class CCanvasTable : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 单元格在不同状态时的颜色 class="type">class="kw">color m_cell_color; class="type">class="kw">color m_cell_color_hover; class=class="str">"cmt">//--- 悬浮时行高亮显示模式 class="type">bool m_lights_hover; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 单元格在不同状态时的颜色 class="type">void CellColor(class="kw">const class="type">class="kw">color clr) { m_cell_color=clr; } class="type">void CellColorHover(class="kw">const class="type">class="kw">color clr) { m_cell_color_hover=clr; } class=class="str">"cmt">//--- 悬浮时行高亮显示模式 class="type">void LightsHover(class="kw">const class="type">bool flag) { m_lights_hover=flag; } }; class CCanvasTable : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 确定行焦点 class="type">int m_item_index_focus; class=class="str">"cmt">//--- 确定鼠标光标从一行转换到另一行的时刻 class="type">int m_prev_item_index_focus; class=class="str">"cmt">//--- class="kw">private: class=class="str">"cmt">//--- 悬停时更改行颜色 class="type">void ChangeRowsColor(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 悬停时更改行颜色 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CCanvasTable::ChangeRowsColor(class="type">void) { class=class="str">"cmt">//--- 如果禁用悬停时的行高亮显示, 离开 if(!m_lights_hover) class="kw">return; class=class="str">"cmt">//--- 如果不在焦点 if(!m_table.MouseFocus()) { class=class="str">"cmt">//--- 如果还未指出不在焦点 if(m_prev_item_index_focus!=WRONG_VALUE) { m_item_index_focus=WRONG_VALUE; class=class="str">"cmt">//--- 改变颜色 RedrawRow(); m_table.Update(); class=class="str">"cmt">//--- 重置焦点 m_prev_item_index_focus=WRONG_VALUE;