图形界面 X: 升级渲染表格及代码优化 (集成编译 10)·综合运用
📘

图形界面 X: 升级渲染表格及代码优化 (集成编译 10)·综合运用

第 3/3 篇

表格行焦点重绘的逻辑分支

在自定义画布表格里,焦点行的颜色刷新靠两段逻辑接力:先判断当前是否处于焦点状态,若否,则通过 CheckRowFocus() 拿鼠标所在行索引,立刻 RedrawRow() 并 m_table.Update(),再把该索引存进 m_prev_item_index_focus。 当已经处于焦点时,代码会取鼠标相对表格的 Y 坐标,用 condition=(y>m_rows[idx].m_y && y<=m_rows[idx].m_y2) 验证光标是否还落在同一行。若不满足条件,说明焦点已跨行,再次调用 CheckRowFocus() 与 RedrawRow() 重画。 RedrawRow() 内部用 is_selected_row 参数切换数据源:选中态读 m_selected_item / m_prev_selected_item,悬停态读 m_item_index_focus / m_prev_item_index_focus。当新旧索引都有效时 rows_total 设为 2(重画两行),否则为 1,列数则固定为 m_columns_total-1。 开 MT5 把这段挂到 EA 的画布控件里,移动鼠标看 row 颜色是否只重绘相邻两行,能验证焦点跨行开销被压到最低。外汇与贵金属图表叠加自绘控件时波动刷新频繁,需注意高频 RedrawRow 带来的 CPU 占用风险。

MQL5 / C++
   else
    {
      class=class="str">"cmt">//--- 检查行上的焦点
      if(m_item_index_focus==WRONG_VALUE)
        {
         class=class="str">"cmt">//--- 获取焦点所在行的索引
         m_item_index_focus=CheckRowFocus();
         class=class="str">"cmt">//--- 改变行颜色
         RedrawRow();
         m_table.Update();
         class=class="str">"cmt">//--- 保存为以前的焦点索引
         m_prev_item_index_focus=m_item_index_focus;
         class="kw">return;
        }
      class=class="str">"cmt">//--- 获取鼠标光标之下的相对 Y 坐标
      class="type">int y=m_mouse.RelativeY(m_table);
      class=class="str">"cmt">//--- 验证焦点
      class="type">bool condition=(y>m_rows[m_item_index_focus].m_y && y<=m_rows[m_item_index_focus].m_y2);
      class=class="str">"cmt">//--- 如果焦点改变了
      if(!condition)
        {
         class=class="str">"cmt">//--- 获取焦点所在行的索引
         m_item_index_focus=CheckRowFocus();
         class=class="str">"cmt">//--- 改变行颜色
         RedrawRow();
         m_table.Update();
         class=class="str">"cmt">//--- 保存为以前的焦点索引
         m_prev_item_index_focus=m_item_index_focus;
        }
    }
class CCanvasTable : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 根据指定的模式重绘指定的表格行
  class="type">void              RedrawRow(class="kw">const class="type">bool is_selected_row=class="kw">false);
  };
class="type">void CCanvasTable::RedrawRow(class="kw">const class="type">bool is_selected_row=class="kw">false)
  {
class=class="str">"cmt">//--- 当前以及前一行的索引
   class="type">int item_index      =WRONG_VALUE;
   class="type">int prev_item_index =WRONG_VALUE;
class=class="str">"cmt">//--- 相对于指定模式初始化行索引
   if(is_selected_row)
     {
      item_index      =m_selected_item;
      prev_item_index =m_prev_selected_item;
     }
   else
     {
      item_index      =m_item_index_focus;
      prev_item_index =m_prev_item_index_focus;
     }
class=class="str">"cmt">//--- 如果没有定义索引, 离开
   if(prev_item_index==WRONG_VALUE && item_index==WRONG_VALUE)
      class="kw">return;
class=class="str">"cmt">//--- 将要绘制的行数和列数
   class="type">int rows_total     =(item_index!=WRONG_VALUE && prev_item_index!=WRONG_VALUE)? class="num">2 : class="num">1;
   class="type">int columns_total =m_columns_total-class="num">1;
class=class="str">"cmt">//--- 坐标
   class="type">int x1=class="num">1,x2=m_table_x_size;
   class="type">int y1[class="num">2]={class="num">0},y2[class="num">2]={class="num">0};
class=class="str">"cmt">//--- 确定序列中的数值数组
   class="type">int indexes[class="num">2];
class=class="str">"cmt">//--- 如果 (class="num">1) 鼠标光标向下移动, 或如果 (class="num">2) 首次进入
   if(item_index>m_prev_item_index_focus || item_index==WRONG_VALUE)
     {

「表格行高亮与网格绘制的光标逻辑」

这段绘制逻辑的核心,是根据鼠标光标相对上一帧的位置决定哪两行进入 indexes 数组:光标向下时,indexes[0] 取上一行索引(除非 item_index 非法且 prev 也非法),indexes[1] 取当前行;向上时顺序反转。这样能保证高亮区域始终覆盖光标跨越的相邻两行,而不是单点闪烁。 行背景用 FillRectangle 按 y1/y2 各缩 1 像素绘制,焦点行由 is_item_focus 判定——非 hover 模式下只认 item_index,hover 模式下若光标离开(WRONG_VALUE)则回退到 prev_item_index。外汇与贵金属图表叠加此类自定义表格时,需注意高亮重绘频率,过高可能在 tick 密集时段拖慢 MT5 终端响应。 网格线遍历每行每列,在列右边界 m_x2 处画竖线;图标仅在存在且列对齐为 ALIGN_LEFT 时由 DrawImage 输出。文本绘制前用 TextX 与 TextAlign 取坐标和对齐方式,y 坐标统一加 m_text_y_offset,保证多列文字基线一致。 直接把这段塞进你自己的 CTable 派生类 OnDraw 里,改 m_lights_hover 默认值,就能在 MT5 上直观对比『跟随光标』与『锁定上次位置』两种高亮行为的差异。

MQL5 / C++
indexes[class="num">0]=(item_index==WRONG_VALUE || prev_item_index!=WRONG_VALUE)? prev_item_index : item_index;
indexes[class="num">1]=item_index;
 }
class=class="str">"cmt">//--- 如果鼠标光标向上移动
 else
  {
   indexes[class="num">0]=item_index;
   indexes[class="num">1]=prev_item_index;
  }
class=class="str">"cmt">//--- 绘制行的背景
 for(class="type">int r=class="num">0; r<rows_total; r++)
  {
   class=class="str">"cmt">//--- 计算行的上、下边界坐标
   y1[r]=m_rows[indexes[r]].m_y+class="num">1;
   y2[r]=m_rows[indexes[r]].m_y2-class="num">1;
   class=class="str">"cmt">//--- 确定处于高亮显示模式的焦点所在行
   class="type">bool is_item_focus=class="kw">false;
   if(!m_lights_hover)
     is_item_focus=(indexes[r]==item_index && item_index!=WRONG_VALUE);
   else
     is_item_focus=(item_index==WRONG_VALUE)?(indexes[r]==prev_item_index) :(indexes[r]==item_index);
   class=class="str">"cmt">//--- 绘制行背景
   m_table.FillRectangle(x1,y1[r],x2,y2[r],RowColorCurrent(indexes[r],is_item_focus));
  }
class=class="str">"cmt">//--- 网格颜色
 class="type">uint clr=::ColorToARGB(m_grid_color);
class=class="str">"cmt">//--- 绘制边界
 for(class="type">int r=class="num">0; r<rows_total; r++)
  {
   for(class="type">int c=class="num">0; c<columns_total; c++)
     m_table.Line(m_columns[c].m_x2,y1[r],m_columns[c].m_x2,y2[r],clr);
  }
class=class="str">"cmt">//--- 绘制图标
 for(class="type">int r=class="num">0; r<rows_total; r++)
  {
   for(class="type">int c=class="num">0; c<m_columns_total; c++)
    {
     class=class="str">"cmt">//--- 绘制图标, 如果 (class="num">1) 它存在于单元格中, 且 (class="num">2) 此列的文本靠左侧对齐
     if(ImagesTotal(c,r)>class="num">0 && m_columns[c].m_text_align==ALIGN_LEFT)
       DrawImage(c,indexes[r]);
    }
  }
class=class="str">"cmt">//--- 计算坐标
 class="type">int x=class="num">0,y=class="num">0;
class=class="str">"cmt">//--- 文本对齐模式
 class="type">uint text_align=class="num">0;
class=class="str">"cmt">//--- 绘制文字
 for(class="type">int c=class="num">0; c<m_columns_total; c++)
  {
   class=class="str">"cmt">//--- 获取 (class="num">1) 文本的 X 坐标, 和 (class="num">2) 文本对齐模式
   x        =TextX(c);
   text_align =TextAlign(c,TA_TOP);
   class=class="str">"cmt">//---
   for(class="type">int r=class="num">0; r<rows_total; r++)
    {
     class=class="str">"cmt">//--- (class="num">1) 计算坐标, 并 (class="num">2) 绘制文本
     y=m_rows[indexes[r]].m_y+m_text_y_offset;
     m_table.TextOut(x,y,m_columns[c].m_rows[indexes[r]].m_short_text,TextColor(c,indexes[r]),text_align);
    }
  }
}

◍ 只刷单元格不刷全表:RedrawCell 与属性改写的用法

表格行级重绘之外,更细粒度的是单元格级重绘。改某个格子的文本、颜色或图标时,没必要重画整张表,私有方法 RedrawCell() 只更新该格内容,边框保持不变;若开了行高亮,它会先按焦点状态算背景色,再依次画背景、图标(仅当分配了且文本左对齐)、文本。 对外改内容用三个公有方法:SetValue() 写文本、TextColor() 写颜色、ChangeImage() 换图标。前两者要传列行索引和值,SetValue() 会把完整串和按格宽截断的短串都存进 CTCell 结构;它们第四个参数 redraw 控制是否立刻重绘,设 false 就等 UpdateTable() 统一刷。ChangeImage() 第三个参数是待切换的图标索引,同样能延后重绘。 UpdateTable() 是唯一公开的全表刷新入口,默认参数 false 表示仅刷新不重绘框架;当多格属性改完再调一次,就能低频统一出图。 实测 5 列 30 行表格在 EA 里以 100 毫秒频率刷所有单元格图标,无用户交互时 CPU 负载不超过 3%。这个开销说明单元格级局部重绘在 MT5 图形界面里是可长期跑的。 下面这段是 RedrawCell 的核心声明与局部重绘逻辑,以及 SetValue 的方法签名,逐行拆一下关键点。

MQL5 / C++
class CCanvasTable : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 重绘指定的表格单元
   class="type">void                RedrawCell(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::RedrawCell(class="kw">const class="type">int column_index,class="kw">const class="type">int row_index)
  {
class=class="str">"cmt">//--- 坐标
   class="type">int x1=m_columns[column_index].m_x+class="num">1;
   class="type">int x2=m_columns[column_index].m_x2-class="num">1;
   class="type">int y1=m_rows[row_index].m_y+class="num">1;
   class="type">int y2=m_rows[row_index].m_y2-class="num">1;
class=class="str">"cmt">//--- 计算坐标
   class="type">int x=class="num">0,y=class="num">0;
class=class="str">"cmt">//--- 检查焦点
   class="type">bool is_row_focus=class="kw">false;
class=class="str">"cmt">//--- 如果启用行高亮显示模式
   if(m_lights_hover)
     {
      class=class="str">"cmt">//--- (class="num">1) 获取鼠标光标的相对 Y 坐标, 和 (class="num">2) 指定表格行上的焦点
      y=m_mouse.RelativeY(m_table);
      is_row_focus=(y>m_rows[row_index].m_y && y<=m_rows[row_index].m_y2);
     }
class=class="str">"cmt">//--- 绘制单元背景
   m_table.FillRectangle(x1,y1,x2,y2,RowColorCurrent(row_index,is_row_focus));
class=class="str">"cmt">//--- 绘制图标, 如果 (class="num">1) 它存在于单元格中, 且 (class="num">2) 此列的文本靠左侧对齐
   if(ImagesTotal(column_index,row_index)>class="num">0 && m_columns[column_index].m_text_align==ALIGN_LEFT)
      DrawImage(column_index,row_index);
class=class="str">"cmt">//--- 获取文本对齐模式
   class="type">uint text_align=TextAlign(column_index,TA_TOP);
class=class="str">"cmt">//--- 绘制文字
   for(class="type">int c=class="num">0; c<m_columns_total; c++)
     {
      class=class="str">"cmt">//--- 获取文本的 X 坐标
      x=TextX(c);
      class=class="str">"cmt">//--- 停止循环
      if(c==column_index)
         class="kw">break;
     }
class=class="str">"cmt">//--- (class="num">1) 计算 Y 坐标, 并 (class="num">2) 绘制文本
   y=y1+m_text_y_offset-class="num">1;
   m_table.TextOut(x,y,m_columns[column_index].m_rows[row_index].m_short_text,TextColor(column_index,row_index),text_align);
  }
class CCanvasTable : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 为指定的表格单元设置数值
   class="type">void                SetValue(class="kw">const class="type">uint column_index,class="kw">const class="type">uint row_index,class="kw">const class="type">class="kw">string value,class="kw">const class="type">bool redraw=class="kw">false);

给表格单元灌数据改颜色换图标

在 MT5 的 CCanvasTable 自定义类里,单元格不是直接画死的,而是通过成员函数往二维结构里写值。SetValue 负责把字符串塞进 m_columns[column_index].m_rows[row_index].m_full_text,同时调用 CorrectingText 生成截断后的 m_short_text,防止文字超出单元格宽度。 TextColor 的逻辑更轻量,只改 m_text_color 这一个属性,不影响文本本身。两个函数都带 redraw 参数,默认 false;如果你在循环里批量改 50 个单元,建议循环结束后再统一重绘一次,而不是每次传 true,能少触发几十次 RedrawCell。 ChangeImage 多了一层防护:先拿 ImagesTotal 看该单元格有没有图标,再比对 image_index 是否越界,最后检查是不是已经选中了同一个图标。若 image_index 等于已有的 m_selected_image 就直接 return,避免无意义的重绘。外汇与贵金属图表叠加这类自定义表格时,注意高频重绘可能拖累 tick 处理性能,属于高风险环境下的细节优化。

MQL5 / C++
class="type">void CCanvasTable::SetValue(class="kw">const class="type">uint column_index,class="kw">const class="type">uint row_index,class="kw">const class="type">class="kw">string value,class="kw">const class="type">bool redraw=class="kw">false)
  {
class=class="str">"cmt">//--- 检查是否超出数组范围
   if(!CheckOutOfRange(column_index,row_index))
      class="kw">return;
class=class="str">"cmt">//--- 将数值存储到数组中
   m_columns[column_index].m_rows[row_index].m_full_text=value;
class=class="str">"cmt">//--- 调整并存储文本, 如果它不适合单元格
   m_columns[column_index].m_rows[row_index].m_short_text=CorrectingText(column_index,row_index);
class=class="str">"cmt">//--- 如果已指定, 重绘单元格
   if(redraw)
      RedrawCell(column_index,row_index);
  }
class="type">void CCanvasTable::TextColor(class="kw">const class="type">uint column_index,class="kw">const class="type">uint row_index,class="kw">const class="type">class="kw">color clr,class="kw">const class="type">bool redraw=class="kw">false)
  {
class=class="str">"cmt">//--- 检查是否超出数组范围
   if(!CheckOutOfRange(column_index,row_index))
      class="kw">return;
class=class="str">"cmt">//--- 将文本颜色存储在公共数组中
   m_columns[column_index].m_rows[row_index].m_text_color=clr;
class=class="str">"cmt">//--- 如果已指定, 重绘单元格
   if(redraw)
      RedrawCell(column_index,row_index);
  }
class="type">void CCanvasTable::ChangeImage(class="kw">const class="type">uint column_index,class="kw">const class="type">uint row_index,class="kw">const class="type">uint image_index,class="kw">const class="type">bool redraw=class="kw">false)
  {
class=class="str">"cmt">//--- 检查是否超出数组范围
   if(!CheckOutOfRange(column_index,row_index))
      class="kw">return;
class=class="str">"cmt">//--- 获取单元格图标的数量
   class="type">int images_total=ImagesTotal(column_index,row_index);
class=class="str">"cmt">//--- 如果 (class="num">1) 没有图标, 或是 (class="num">2) 超出范围, 离开
   if(images_total==WRONG_VALUE || image_index>=(class="type">uint)images_total)
      class="kw">return;
class=class="str">"cmt">//--- 如果指定的图标与已选定的图标匹配, 离开
   if(image_index==m_columns[column_index].m_rows[row_index].m_selected_image)
      class="kw">return;
  }

「单元格图标索引与表格更新的底层写法」

在自定义表格控件里,单元格选中图标的记录依赖一个强制类型转换:把传入的 image_index 转成 int 再写进 m_selected_image。这一步看着简单,但漏了转换在严格编译模式下会直接报错,开 MT5 写同类 CCanvasTable 派生类时值得照抄。 重绘不是无脑全量跑。代码里用 redraw 参数 gate 住 RedrawCell 和 DrawTable 的调用——只在该布尔量为 true 时才触发绘制函数,否则只更新内存里的数据结构。实盘面板若每帧都重画,CPU 占用会明显抬升,这个开关能省掉不必要的 GDI 开销。 UpdateTable 作为私有方法,对外只暴露 redraw 默认值 false。意味着常规数据刷新不联动画图,要肉眼看到变化得显式传 true。调试时若发现表格数据改了界面没动,先查调用处有没有把 redraw 置真。

MQL5 / C++
class=class="str">"cmt">//--- 保存单元格所选图标的索引
   m_columns[column_index].m_rows[row_index].m_selected_image=(class="type">int)image_index;
class=class="str">"cmt">//--- 如果已指定, 重绘单元格
   if(redraw)
      RedrawCell(column_index,row_index);
   }
class CCanvasTable : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 更新表格
   class="type">void              UpdateTable(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 CCanvasTable::UpdateTable(class="kw">const class="type">bool redraw=class="kw">false)
  {
class=class="str">"cmt">//--- 如果指定,重绘表格
   if(redraw)
      DrawTable();
class=class="str">"cmt">//--- 更新表格
   m_table.Update();
   }

◍ 用自绘表格复刻市场观察窗口

MT5 当前版本的图形渲染能力,已经足以在自定义面板里画出和「市场观察」窗口同构的报价表。实测在 MetaQuotes-Demo 服务器上取 25 个可用品种,铺成 5 列 25 行,列定义为 Symbol、Bid、Ask、Spread(!)、Time,就能对齐原生窗口的数据结构。 表格单元首次初始化放在控件创建方法里直接跑,由 CProgram::InitializingTable() 这个辅助方法接管。运行期默认刷新率是 16 毫秒,另写 CProgram::UpdateTable() 负责周期更新:周末(周六、周日)直接 return,否则双重循环扫所有行列,抓取每个品种最近两笔 tick,比完价格方向再写单元格。 图标复用原生逻辑——灰圈表示无变化,上箭头表示买价上行,下箭头表示买价下行。下面这段初始化代码可直接拷进 EA 的 CProgram 类里验证,注意 image_array 路径依赖 EasyAndFastGUI 资源包,缺了图就只显示文字。 别把 16 毫秒当万能刷频 外汇与贵金属报价在高波动时段可能丢 tick,16ms 刷新在普通品种上够用,但遇到点差剧烈扩张时,自绘表里的 Spread 列可能滞后于真实成交价,仅作监控参考而非下单依据。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建应用程序的类                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
   class=class="str">"cmt">//--- 初始化表格
   class="type">void              InitializingTable(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 初始化表格                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::InitializingTable(class="type">void)
  {
class=class="str">"cmt">//--- 标题数组
   class="type">class="kw">string text_headers[COLUMNS1_TOTAL]={"Symbol","Bid","Ask","!","Time"};
class=class="str">"cmt">//--- 品种数组
   class="type">class="kw">string text_array[class="num">25]=
     {
       "AUDUSD","GBPUSD","EURUSD","USDCAD","USDCHF","USDJPY","NZDUSD","USDSEK","USDHKD","USDMXN",
       "USDZAR","USDTRY","GBPAUD","AUDCAD","CADCHF","EURAUD","GBPCHF","GBPJPY","NZDJPY","AUDJPY",
       "EURJPY","EURCHF","EURGBP","AUDCHF","CHFJPY"
     };
class=class="str">"cmt">//--- 图标数组
   class="type">class="kw">string image_array[class="num">3]=
     {
       "::Images\\EasyAndFastGUI\\Icons\\bmp16\\circle_gray.bmp",
       "::Images\\EasyAndFastGUI\\Icons\\bmp16\\arrow_up.bmp",
       "::Images\\EasyAndFastGUI\\Icons\\bmp16\\arrow_down.bmp"
     };
class=class="str">"cmt">//---
   for(class="type">int c=class="num">0; c<COLUMNS1_TOTAL; c++)
     {
      class=class="str">"cmt">//--- 设置标题
      m_canvas_table.SetHeaderText(c,text_headers[c]);
      class=class="str">"cmt">//---
      for(class="type">int r=class="num">0; r<ROWS1_TOTAL; r++)
        {
         class=class="str">"cmt">//--- 设置图标
         m_canvas_table.SetImages(c,r,image_array);
         class=class="str">"cmt">//--- 设置品名
         if(c<class="num">1)
            m_canvas_table.SetValue(c,r,text_array[r]);
         class=class="str">"cmt">//--- 所有单元的省缺值
         else
            m_canvas_table.SetValue(c,r,"-");
        }
     }
  }
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
   class=class="str">"cmt">//--- 初始化表格
   class="type">void              InitializingTable(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+

用两笔报价判定多空与刷新点差

表格实时刷新的核心在 UpdateTable:先拿服务器时间,遇周六日直接 return,避免非交易时段空跑浪费资源。 逐行列遍历时,对每个品种只取最近 2 笔 tick(CopyTicks 的参数为 COPY_TICKS_ALL、起始 0、数量 2)。若不足两笔则 continue 跳过,保证后面比价逻辑不越界。 方向判定只看 bid:ticks[0].bid 与 ticks[1].bid 相等标 0(不动),大于标 1(买压倾向上行),小于标 2(卖压倾向下行)。注意这里用 ArraySetAsSeries(ticks,true) 把数组翻成时间序列,所以索引 0 是最新、1 是前一笔。 第 3 列单独处理点差,用 SymbolInfoInteger(symbol,SYMBOL_SPREAD) 取整数点值。外汇与贵金属点差随流动性剧烈跳动,周末外盘休市时价差可能瞬间扩大数倍,属高风险特征,实盘需以经纪商实时值为准。

MQL5 / C++
class="type">void CProgram::UpdateTable(class="type">void)
  {
  class="type">MqlDateTime check_time;
  ::TimeToStruct(::TimeTradeServer(),check_time);
class=class="str">"cmt">//--- 如果是星期六或星期天, 离开
  if(check_time.day_of_week==class="num">0 || check_time.day_of_week==class="num">6)
    class="kw">return;
class=class="str">"cmt">//---
  for(class="type">int c=class="num">0; c<m_canvas_table.ColumnsTotal(); c++)
    {
    for(class="type">int r=class="num">0; r<m_canvas_table.RowsTotal(); r++)
      {
      class=class="str">"cmt">//--- 获取数据的品种
      class="type">class="kw">string symbol=m_canvas_table.GetValue(class="num">0,r);
      class=class="str">"cmt">//--- 获取最后两笔即时报价的数据
      class="type">MqlTick ticks[];
      if(::CopyTicks(symbol,ticks,COPY_TICKS_ALL,class="num">0,class="num">2)<class="num">2)
        class="kw">continue;
      class=class="str">"cmt">//--- 将数组设置为时间序列
      ::ArraySetAsSeries(ticks,true);
      class=class="str">"cmt">//--- 品种列 - 品种。Determine the price direction.
      if(c==class="num">0)
        {
        class="type">int index=class="num">0;
        class=class="str">"cmt">//--- 如果价格未变化
        if(ticks[class="num">0].ask==ticks[class="num">1].ask && ticks[class="num">0].bid==ticks[class="num">1].bid)
          index=class="num">0;
        class=class="str">"cmt">//--- 如果供给价格向上变化
        else if(ticks[class="num">0].bid>ticks[class="num">1].bid)
          index=class="num">1;
        class=class="str">"cmt">//--- 如果供给价格向下变化
        else if(ticks[class="num">0].bid<ticks[class="num">1].bid)
          index=class="num">2;
        class=class="str">"cmt">//--- 设置相应的图标
        m_canvas_table.ChangeImage(c,r,index,true);
        }
      else
        {
        class=class="str">"cmt">//--- 价格差异列 - 点差 (!)
        if(c==class="num">3)
          {
          class=class="str">"cmt">//--- 获取并设置点差的点数大小
          class="type">int spread=(class="type">int)::SymbolInfoInteger(symbol,SYMBOL_SPREAD);

「逐列刷新报价面板的着色逻辑」

这段循环负责把每个交易品种的不同字段写进 Canvas 表格。先取 SYMBOL_DIGITS 决定小数位,再用 DoubleToString 把 bid / ask 格式化成对应精度——黄金 XAUUSD 通常是 2 位,EURUSD 是 5 位,写错位数界面会错位。 bid 列(c==1)和 ask 列(c==2)都做了价格变动检测:拿当前 tick 和上一 tick 比,若新值小于旧值染红、大于旧值染蓝。这样盘口跳动时,交易员扫一眼颜色就知道主动抛压还是主动买盘占优,外汇与贵金属波动剧烈,颜色只是辅助,不代表方向确认。 时间列(c==4)把 SymbolInfoInteger 的 SYMBOL_TIME 和 tick 的 time_msc 拼起来,只截最后 3 位毫秒补在秒后面。例如 TIME_SECONDS 给出 14:23:07,毫秒 482 就显示成 14:23:07.482,方便你核对 MT5 tick 到达的真实密度。

MQL5 / C++
m_canvas_table.SetValue(c,r,class="type">class="kw">string(spread),true);
class="kw">continue;
}
class=class="str">"cmt">//--- 获取小数位数
class="type">int digit=(class="type">int)::SymbolInfoInteger(symbol,SYMBOL_DIGITS);
class=class="str">"cmt">//--- 供给价列
if(c==class="num">1)
  {
   m_canvas_table.SetValue(c,r,::DoubleToString(ticks[class="num">0].bid,digit));
   class=class="str">"cmt">//--- 如果价格有变化, 设定与方向对应的颜色
   if(ticks[class="num">0].bid!=ticks[class="num">1].bid)
     m_canvas_table.TextColor(c,r,(ticks[class="num">0].bid<ticks[class="num">1].bid)? clrRed : clrBlue,true);
   class=class="str">"cmt">//---
   class="kw">continue;
   }
class=class="str">"cmt">//--- 采购价列
if(c==class="num">2)
  {
   m_canvas_table.SetValue(c,r,::DoubleToString(ticks[class="num">0].ask,digit));
   class=class="str">"cmt">//--- 如果价格有变化, 设定与方向对应的颜色
   if(ticks[class="num">0].ask!=ticks[class="num">1].ask)
     m_canvas_table.TextColor(c,r,(ticks[class="num">0].ask<ticks[class="num">1].ask)? clrRed : clrBlue,true);
   class=class="str">"cmt">//---
   class="kw">continue;
   }
class=class="str">"cmt">//--- 最后品种价格的到达时刻列
if(c==class="num">4)
  {
   class="type">long   time      =::SymbolInfoInteger(symbol,SYMBOL_TIME);
   class="type">class="kw">string time_msc =::IntegerToString(ticks[class="num">0].time_msc);
   class="type">int    length   =::StringLen(time_msc);
   class="type">class="kw">string msc      =::StringSubstr(time_msc,length-class="num">3,class="num">3);
   class="type">class="kw">string str      =::TimeToString(time,TIME_MINUTES|TIME_SECONDS)+"."+msc;
   class=class="str">"cmt">//---
   class="type">class="kw">color clr=clrBlack;

◍ 用买卖盘跳动给报价表上色

在 MT5 的自定义报价面板里,光看数字不够直观,得靠颜色把供给端的变动速度显出来。下面这段逻辑就是拿相邻两笔 tick 的 bid 做比较,决定单元格染什么色。 如果最新 tick 的 ask 和 bid 都和上一笔完全相等,说明价格静止,单元格置为黑色 clrBlack;若最新 bid 大于前一笔 bid,意味着供给价格被向上吃掉,染成蓝色 clrBlue;反之 bid 走低则染红 clrRed。 [CODE] //--- 如果价格未变化 if(ticks[0].ask==ticks[1].ask && ticks[0].bid==ticks[1].bid) clr=clrBlack; //--- 如果供给价格向上变化 else if(ticks[0].bid>ticks[1].bid) clr=clrBlue; //--- 如果供给价格向下变化 else if(ticks[0].bid<ticks[1].bid) clr=clrRed; //--- 设置数值和文本颜色 m_canvas_table.SetValue(c,r,str); m_canvas_table.TextColor(c,r,clr,true); continue; } } } } //--- 更新表格 m_canvas_table.UpdateTable(); } [/CODE] 逐行拆一下:ticks[0] 是刚到的 tick,ticks[1] 是上一笔;双等号判断买卖盘都未动才黑屏。else if 里只比 bid 不比 ask,是因为供给侧挂单厚度变化最先反映在 bid 上。SetValue 写文本,TextColor 的第三个参数 true 表示立即重绘,continue 跳过后续列直接跑下一格。 最后 m_canvas_table.UpdateTable() 把整张 Canvas 表格推到图表。实盘里外汇和贵金属点差跳动快,这种染色法能让你一眼看出哪一行供给在撤、哪一行在被扫,但高频下红色闪烁可能只是噪音,需结合成交量阈值过滤。

MQL5 / C++
class=class="str">"cmt">//--- 如果价格未变化
if(ticks[class="num">0].ask==ticks[class="num">1].ask && ticks[class="num">0].bid==ticks[class="num">1].bid)
   clr=clrBlack;
class=class="str">"cmt">//--- 如果供给价格向上变化
else if(ticks[class="num">0].bid>ticks[class="num">1].bid)
   clr=clrBlue;
class=class="str">"cmt">//--- 如果供给价格向下变化
else if(ticks[class="num">0].bid<ticks[class="num">1].bid)
   clr=clrRed;
class=class="str">"cmt">//--- 设置数值和文本颜色
m_canvas_table.SetValue(c,r,str);
m_canvas_table.TextColor(c,r,clr,true);
class="kw">continue;
}
   }
      }
   }
class=class="str">"cmt">//--- 更新表格
m_canvas_table.UpdateTable();
}

别急着下结论

这套 GUI 函数库当前阶段的结构已在附图里摊开,最新包 easyandfastgui_mql5.zip 体积 578.26 KB,可直接拉到 MT5 里跑一遍看渲染效率。 有用户在 2018 年反馈:把 MetaEditor 的 metaeditor.ini 里 Optimize 参数置 0 后,编译耗时压到 351 毫秒、0 报错 0 警告;但 1755/1816 版客户端未必带这个字段,得用记事本搜文件才找得到。 外汇与贵金属行情波动剧烈、杠杆风险高,这类界面库只解决交互层问题,信号逻辑仍要你自己回测验证。先搭环境编译通,再谈策略不迟。

常见问题

在光标逻辑里判断当前行索引是否等于焦点行,仅对该行调用高亮重绘分支,其余行保持原网格绘制,避免全表刷新卡顿。
要用 RedrawCell 单独刷新该单元格并改写其属性,而不是等全表重绘;只改数据不触发单元格重绘就不会即时生效。
可以,把表格代码贴给小布,它能指出 RedrawCell 漏写、图标索引越界等底层写法问题,并给出修改建议。
光标进入行区域时记录焦点行并画高亮底边,移出恢复网格;网格绘制放在非焦点行,保证视觉层次且刷新轻量。
先写图标索引到单元属性再调用更新,否则可能显示旧图标;索引越界会绘制失败,需先做范围校验。