图形界面 X: 排序、重建表格和单元格中的控件 (集成编译 11)·综合运用
📘

图形界面 X: 排序、重建表格和单元格中的控件 (集成编译 11)·综合运用

第 3/3 篇

「用计数器捕捉图表双击」

在 MT5 自定义指标或 EA 里,想区分用户是单击还是双击图表,可以靠两次按下事件的时间差来判断。这里用 curr_depressed - prev_depressed 算出间隔计数,再和预设的 m_pause_between_clicks 阈值比较。 若间隔小于阈值,就通过 EventChartCustom 向图表抛出 ON_DOUBLE_CLICK 自定义事件,把计数器作为 lparam 传出去,方便后续逻辑处理。外汇与贵金属行情跳动快,这类交互检测建议先在模拟盘验证,误触可能倾向引发非预期下单。

MQL5 / C++
  class="type">uint counter=curr_depressed-prev_depressed;
class=class="str">"cmt">//--- 如果两次点击之间的时间少于指定的时间, 发送有关双击的消息
  if(counter<m_pause_between_clicks)
      ::EventChartCustom(m_chart.ChartId(),ON_DOUBLE_CLICK,counter,class="num">0.0,"");
  }

把复选框和按钮塞进表格单元

做多参数 EA 的图形面板时,用表格排布参数更直观,但默认单元只是 CELL_SIMPLE,也就是纯文本无控件。要让单元承载交互,先得在枚举里区分类型:CELL_SIMPLE=0 是空单元,CELL_BUTTON=1 是按钮,CELL_CHECKBOX=2 是复选框。 初始化后可用 CellType() 读写某行某列的类型。复选框单元至少得挂 2 张图(未勾/已勾),也可以多放几张做多状态;按钮单元最少 1 张图。因为不同列控件图尺寸可能不一,要用 ImageXOffset()、ImageYOffset() 按列单独设偏移,否则图会叠歪。 点击判定从老的 OnClickTable() 里抽出了 PressedRowIndex() 和 PressedCellColumnIndex(),分别拿到被点行、列索引。私有方法 CheckCellElement() 接收这两个索引和是否双击的标志,再按单元类型分流到 CheckPressedButton() 或 CheckPressedCheckBox()。 两个处理方法开头都先查图像数量是否达标,再判点击是否落在图上。复选框支持两种切换:单击图标直接toggle,双击单元任意处也行。条件满足后,方法会抛出带控件标识符的事件,图像索引走 double 参数,行列号拼成 string 参数传出去。 外汇和贵金属市场杠杆高、波动剧烈,这类界面只解决交互逻辑,不涉及任何交易信号或收益承诺,实盘前请在 MT5 策略测试器里先验证点击事件投递是否符合预期。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 表格单元类型枚举                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_TYPE_CELL
  {
   CELL_SIMPLE   =class="num">0,
   CELL_BUTTON   =class="num">1,
   CELL_CHECKBOX =class="num">2
  };
class CCanvasTable : class="kw">public CElement
  {
   class=class="str">"cmt">//--- 设置/获取单元类型
   class="type">void              CellType(class="kw">const class="type">uint column_index,class="kw">const class="type">uint row_index,class="kw">const ENUM_TYPE_CELL type);
   ENUM_TYPE_CELL    CellType(class="kw">const class="type">uint column_index,class="kw">const class="type">uint row_index);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置单元类型                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvasTable::CellType(class="kw">const class="type">uint column_index,class="kw">const class="type">uint row_index,class="kw">const ENUM_TYPE_CELL type)
  {
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_type=type;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 获取单元类型                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
ENUM_TYPE_CELL CCanvasTable::CellType(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(m_columns[column_index].m_rows[row_index].m_type);
  }
class CCanvasTable : class="kw">public CElement
  {
class="kw">public:
   class=class="str">"cmt">//--- 图像沿 X 轴和 Y 轴的偏移量
   class="type">void              ImageXOffset(class="kw">const class="type">int &array[]);

◍ 表格单元里的按钮和复选框怎么被捕获

在 MT5 自绘表格(CCanvasTable)里,单元格不一定只是纯文本。当某个单元格挂了控件,点击时要先由 CheckCellElement 判断该格是不是可交互类型,否则直接当普通格处理。 代码逻辑很直接:若 m_type 等于 CELL_SIMPLE 就 return(false),说明这格没控件;否则进 switch,按 CELL_BUTTON 调 CheckPressedButton、按 CELL_CHECKBOX 调 CheckPressedCheckBox,任一步返回 false 则整函数退出。 [CODE]bool CCanvasTable::CheckCellElement(const int column_index,const int row_index,const bool double_click=false) { if(m_columns[column_index].m_rows[row_index].m_type==CELL_SIMPLE) return(false); switch(m_columns[column_index].m_rows[row_index].m_type) { case CELL_BUTTON : { if(!CheckPressedButton(column_index,row_index,double_click)) return(false); break; } case CELL_CHECKBOX : { if(!CheckPressedCheckBox(column_index,row_index,double_click)) return(false); break; } } return(true); }[/CODE] 逐行拆解:函数头接收列索引、行索引和双击标记;第一行判简单格直接溜;switch 吃单元格类型;CELL_BUTTON 分支里调按钮命中检测,失败就撤;CELL_CHECKBOX 同理走复选框检测;全过完才 return(true),代表这格控件被成功激活。 想验证的话,在 MT5 指标里建个带按钮列的 CCanvasTable,点按钮格看是否触发而你点空白格无反应——外汇和贵金属图表上做这种交互面板属高风险环境,参数没隔离好容易误触下单逻辑。

MQL5 / C++
class="type">bool CCanvasTable::CheckCellElement(class="kw">const class="type">int column_index,class="kw">const class="type">int row_index,class="kw">const class="type">bool double_click=class="kw">false)
  {
  if(m_columns[column_index].m_rows[row_index].m_type==CELL_SIMPLE)
     class="kw">return(class="kw">false);
  class="kw">switch(m_columns[column_index].m_rows[row_index].m_type)
     {
     case CELL_BUTTON :
       {
       if(!CheckPressedButton(column_index,row_index,double_click))
          class="kw">return(class="kw">false);
       class="kw">break;
       }
     case CELL_CHECKBOX :
       {
       if(!CheckPressedCheckBox(column_index,row_index,double_click))
          class="kw">return(class="kw">false);
       class="kw">break;
       }
     }
  class="kw">return(true);
  }

「表格单元里的点击命中判定怎么写」

在 MT5 自定义表格(CCanvasTable)里,按钮和复选框都不是真实控件,而是画在 canvas 上的图像。要判断用户到底点没点中,核心是先确认该单元分配了足够图像,再拿鼠标相对坐标去比对图像左右边界。 按钮要求单元至少 1 张图,复选框要求至少 2 张(未选/已选各一)。若图像数不够,函数直接 Print 报错并返回 false,不会往下走命中逻辑。 命中判定的关键数据是:用 m_mouse.RelativeX(m_table) 取光标相对表左上角的 X,再算 image_x 到 image_x2 的像素区间。X 超出 image_x2 即视为点空,立刻返回 false。 按钮在单击命中时会发 EventChartCustom,自定义事件码 ON_CLICK_BUTTON,附带列_行字符串;复选框则额外维护 m_selected_image 索引做状态切换。外汇与贵金属图表挂这类交互表格属高风险操作环境,建议先在策略测试器里用模拟点击验证事件触发。

MQL5 / C++
class="type">bool CCanvasTable::CheckPressedButton(class="kw">const class="type">int column_index,class="kw">const class="type">int row_index,class="kw">const class="type">bool double_click=class="kw">false)
  {
class=class="str">"cmt">//--- 如果单元中没有图像, 离开
   if(ImagesTotal(column_index,row_index)<class="num">1)
     {
      ::Print(__FUNCTION__," > 至少需要分配一个图像给单元按钮!");
      class="kw">return(class="kw">false);
     }
class=class="str">"cmt">//--- 获取鼠标光标下的相对坐标
   class="type">int x=m_mouse.RelativeX(m_table);
class=class="str">"cmt">// --- 获取图像的右边框
   class="type">int image_x  =class="type">int(m_columns[column_index].m_x+m_columns[column_index].m_image_x_offset);
   class="type">int image_x2 =class="type">int(image_x+m_columns[column_index].m_rows[row_index].m_images[class="num">0].m_image_width);
class=class="str">"cmt">//--- 如果点击并不在图像上, 离开
   if(x>image_x2)
      class="kw">return(class="kw">false);
   else
     {
      class=class="str">"cmt">//--- 如果这不是双击, 则发送一条消息
      if(!double_click)
        {
         class="type">int image_index=m_columns[column_index].m_rows[row_index].m_selected_image;
         ::EventChartCustom(m_chart_id,ON_CLICK_BUTTON,CElementBase::Id(),image_index,class="type">class="kw">string(column_index)+"_"+class="type">class="kw">string(row_index));
        }
     }
class=class="str">"cmt">//---
   class="kw">return(true);
  }

class="type">bool CCanvasTable::CheckPressedCheckBox(class="kw">const class="type">int column_index,class="kw">const class="type">int row_index,class="kw">const class="type">bool double_click=class="kw">false)
  {
class=class="str">"cmt">//--- 如果单元中没有图像, 离开
   if(ImagesTotal(column_index,row_index)<class="num">2)
     {
      ::Print(__FUNCTION__," > 至少需要分配两个图像给单元复选框!");
      class="kw">return(class="kw">false);
     }
class=class="str">"cmt">//--- 获取鼠标光标下的相对坐标
   class="type">int x=m_mouse.RelativeX(m_table);
class=class="str">"cmt">// --- 获取图像的右边框
   class="type">int image_x  =class="type">int(m_columns[column_index].m_x+m_image_x_offset);
   class="type">int image_x2 =class="type">int(image_x+m_columns[column_index].m_rows[row_index].m_images[class="num">0].m_image_width);
class=class="str">"cmt">//--- 如果 (class="num">1) 点击不在图像上, 且 (class="num">2) 它不是双击, 离开
   if(x>image_x2 && !double_click)
      class="kw">return(class="kw">false);
   else
     {
      class=class="str">"cmt">//--- 所选图像的当前索引
      class="type">int image_i=m_columns[column_index].m_rows[row_index].m_selected_image;
      class=class="str">"cmt">//--- 确定图像的下一个索引

表格点击与双击的事件分流逻辑

在自定义表格控件里,单击和双击走的是两套独立处理器。OnClickTable 先排掉三种干扰:列宽拖动未结束、垂直或水平滚动条正被拖拽、点击对象名和表格名对不上,任一命中直接返回 false,避免误触发选中。 若开启了整行可选(m_selectable_row 为真)且单元格内没有激活的子控件,单击会重绘整行高亮并调用 m_table.Update(),随后用 EventChartCustom 抛出 ON_CLICK_LIST_ITEM 事件,附加参数里拼了 "列_行" 字符串,EA 或脚本可据此知道用户点了哪一格。 双击处理器 OnDoubleClickTable 更轻量:只要求表格处于鼠标焦点,然后取点击行列,把是否命中单元格控件的结果透传出来——第三个参数传 true 意味着这次是双击语境下的检查。 注意 CheckCellElement(c,r,true) 的返回值既是"是否命中控件"的判断,也是整个双击函数的返回,所以双击空白格会返回 false,只有戳中带控件的单元格才返回 true。 下面这段是图片轮播与表格点击衔接的片段,以及两个处理器的类声明与实现,可直接粘进 MT5 的 include 工程核对行为:

MQL5 / C++
class="type">int next_i=(image_i<ImagesTotal(column_index,row_index)-class="num">1)? ++image_i : class="num">0;
class=class="str">"cmt">//--- 选择下一张图片并更新表格
ChangeImage(column_index,row_index,next_i,true);
m_table.Update(class="kw">false);
class=class="str">"cmt">//--- 发送有关消息
::EventChartCustom(m_chart_id,ON_CLICK_CHECKBOX,CElementBase::Id(),next_i,class="type">class="kw">string(column_index)+"_"+class="type">class="kw">string(row_index));
 }
class=class="str">"cmt">//---
  class="kw">return(true);
  }
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=class="str">"cmt">//--- 处理在表格上的双击
  class="type">bool            OnDoubleClickTable(class="kw">const class="type">class="kw">string clicked_object);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 处理在表格上的点击                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CCanvasTable::OnClickTable(class="kw">const class="type">class="kw">string clicked_object)
  {
class=class="str">"cmt">//--- 如果 (class="num">1) 行选择模式被禁用, 或 (class="num">2) 在更改列宽度的过程中, 离开
  if(m_column_resize_control!=WRONG_VALUE)
    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">//--- 确定点击的行
  class="type">int r=PressedRowIndex();
class=class="str">"cmt">//--- 确定点击的单元格
  class="type">int c=PressedCellColumnIndex();
class=class="str">"cmt">//--- 检查单元格中的控件是否已激活
  class="type">bool is_cell_element=CheckCellElement(c,r);
class=class="str">"cmt">//--- 如果 (class="num">1) 行选择模式被启用, 且 (class="num">2) 单元控件未激活
  if(m_selectable_row && !is_cell_element)
    {
    class=class="str">"cmt">//--- 改变颜色
    RedrawRow(true);
    m_table.Update();
    class=class="str">"cmt">//--- 发送有关消息
    ::EventChartCustom(m_chart_id,ON_CLICK_LIST_ITEM,CElementBase::Id(),m_selected_item,class="type">class="kw">string(c)+"_"+class="type">class="kw">string(r));
    }
class=class="str">"cmt">//---
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 处理在表格上的双击                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CCanvasTable::OnDoubleClickTable(class="kw">const class="type">class="kw">string clicked_object)
  {
  if(!m_table.MouseFocus())
    class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 确定点击的行
  class="type">int r=PressedRowIndex();
class=class="str">"cmt">//--- 确定点击的单元格
  class="type">int c=PressedCellColumnIndex();
class=class="str">"cmt">//--- 检查单元格中的控件是否已激活并返回结果
  class="kw">return(CheckCellElement(c,r,true));
  }

◍ 表格双击事件的拦截与分发

在 MT5 自定义图形面板里,鼠标左键双击不会自动落到表格控件,必须靠 CHARTEVENT_CUSTOM 偏移事件来捕获。上面这段 CCanvasTable::OnEvent 的实现,核心就是判断 id 是否等于 CHARTEVENT_CUSTOM+ON_DOUBLE_CLICK,命中后才进入表格自身的双击处理逻辑。 OnDoubleClickTable(sparam) 接收的是图表事件附带的字符串参数,通常编码了被点击单元格的坐标或对象名。若它返回 true,说明双击落在表格有效区域内,函数直接 return 不再外传;否则也 return,避免事件被误派发到其他监听器。 实盘写面板时,建议把 ON_DOUBLE_CLICK 的数值在枚举里单独定义,别直接写魔法数。这样你开 MT5 挂上 EA,双击表格单元格就能稳定触发回调,而不会和拖动、单击事件打架。外汇与贵金属图表波动剧烈,这类交互延迟虽小,但高频操作下仍可能错失价位。

MQL5 / C++
class="type">void CCanvasTable::OnEvent(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(id==CHARTEVENT_CUSTOM+ON_DOUBLE_CLICK)
     {
      class=class="str">"cmt">//--- 点击表格
      if(OnDoubleClickTable(sparam))
         class="kw">return;
      class=class="str">"cmt">//---
      class="kw">return;
     }
  }

「面板函数的调用链路与注释掉的菜单」

这套 GUI 函数库走到当前阶段,核心是一个 CProgram::CreateExpertPanel 方法,把所有控件按坐标堆到窗口里。窗口名写死为 "EXPERT PANEL",状态栏高度 25、主按钮区从 y=50 开始排,说明布局是写死像素点而非自适应。 注意被 /* */ 整段注释掉的主菜单和四个上下文菜单创建调用。社区里有人反馈:如果把菜单相关代码也删掉,表格就排不了序、复选框也点不动——说明表格交互事件大概率挂在菜单的消息路由上,不是独立控件。 下面这段是面板创建函数的真实代码,注释行已保留,可照着在 MT5 里建个 EA 把菜单段取消注释对比行为差异。外汇与贵金属品种波动剧烈,拿这类界面做批量下单前务必先在策略测试器跑通事件响应。

MQL5 / C++
<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">bool</span> CProgram::CreateExpertPanel(<span class="keyword">class="type">void</span>)
&nbsp;&nbsp;{
<span class="comment">class=class="str">"cmt">//--- 为控件创建表格 class="num">1</span>
&nbsp;&nbsp; <span class="keyword">if</span>(!CreateWindow1(<span class="class="type">class="kw">string">"EXPERT PANEL"</span>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>);
<span class="comment">class=class="str">"cmt">/*/--- 创建控件:
class=class="str">"cmt">// 主菜单
if(!CreateMenuBar(class="num">1,class="num">20))
class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 上下文菜单
if(!CreateMBContextMenu1())
class="kw">return(class="kw">false);
if(!CreateMBContextMenu2())
class="kw">return(class="kw">false);
if(!CreateMBContextMenu3())
class="kw">return(class="kw">false);
if(!CreateMBContextMenu4())
class="kw">return(class="kw">false);*/</span>
<span class="comment">class=class="str">"cmt">//--- 创建状态栏</span>
&nbsp;&nbsp; <span class="keyword">if</span>(!CreateStatusBar(<span class="number">class="num">1</span>,<span class="number">class="num">25</span>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>);
<span class="comment">class=class="str">"cmt">//--- 按钮</span>
&nbsp;&nbsp; <span class="keyword">if</span>(!CreateSimpleButton1(<span class="number">class="num">7</span>,<span class="number">class="num">50</span>,<span class="class="type">class="kw">string">"ADD COLUMN"</span>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>);
&nbsp;&nbsp; <span class="keyword">if</span>(!CreateSimpleButton2(<span class="number">class="num">7</span>,<span class="number">class="num">75</span>,<span class="class="type">class="kw">string">"DELETE COLUMN"</span>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>);
&nbsp;&nbsp; <span class="keyword">if</span>(!CreateSimpleButton3(<span class="number">class="num">180</span>,<span class="number">class="num">50</span>,<span class="class="type">class="kw">string">"ADD ROW"</span>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>);
&nbsp;&nbsp; <span class="keyword">if</span>(!CreateSimpleButton4(<span class="number">class="num">180</span>,<span class="number">class="num">75</span>,<span class="class="type">class="kw">string">"DELETE ROW"</span>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>);
&nbsp;&nbsp; <span class="keyword">if</span>(!CreateSimpleButton5(<span class="number">class="num">353</span>,<span class="number">class="num">50</span>,<span class="class="type">class="kw">string">"REBUILD TABLE"</span>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>);
&nbsp;&nbsp; <span class="keyword">if</span>(!CreateSimpleButton6(<span class="number">class="num">353</span>,<span class="number">class="num">75</span>,<span class="class="type">class="kw">string">"CLEAR TABLE"</span>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>);
<span class="comment">class=class="str">"cmt">//--- 编辑</span>
&nbsp;&nbsp; <span class="keyword">if</span>(!CreateSpinEdit1(<span class="number">class="num">114</span>,<span class="number">class="num">52</span>,<span class="class="type">class="kw">string">":"</span>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>);
&nbsp;&nbsp; <span class="keyword">if</span>(!CreateSpinEdit2(<span class="number">class="num">114</span>,<span class="number">class="num">77</span>,<span class="class="type">class="kw">string">":"</span>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>);
&nbsp;&nbsp; <span class="keyword">if</span>(!CreateSpinEdit3(<span class="number">class="num">287</span>,<span class="number">class="num">52</span>,<span class="class="type">class="kw">string">":"</span>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>);
&nbsp;&nbsp; <span class="keyword">if</span>(!CreateSpinEdit4(<span class="number">class="num">287</span>,<span class="number">class="num">77</span>,<span class="class="type">class="kw">string">":"</span>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>);
<span class="comment">class=class="str">"cmt">//--- 创建渲染表</span>

最后说句实在话

上面那段收尾代码里,CreateCanvasTable(1,103) 的 103 是表格行数上限,42 被注释掉说明早期版本试过更窄的布局,现在直接拉到 103 行才够装下全品种监控。 调用 m_chart.Redraw() 不是可有可无——不重绘的话,刚建好的画布表在 MT5 上可能迟迟不刷新,肉眼看着像卡死。 外汇和贵金属这类高波动品种,用这套画布表盯盘时别迷信它能替你判断方向,它只负责把数据摆清楚;真要下单,仍得自己扛住滑点和跳空的高风险。 下次开 MT5 把这段接进 EA 尾段,跑一遍就知道 103 行在你的分辨率下会不会溢出边框。

MQL5 / C++
  if(!CreateCanvasTable(class="num">1,class="num">103)) class=class="str">"cmt">// class="num">103 | class="num">42
        class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 重新绘制图表
  m_chart.Redraw();
  class="kw">return(true);
}

常见问题

用计数器记录两次点击的时间间隔和坐标,间隔小于系统双击阈值且位置重合判为双击,否则走单击逻辑分流。
需在单元格命中判定里单独检测控件区域,把点击坐标映射到该单元内的子控件矩形,再触发对应回调。
可以,小布能扫描你的界面事件代码,标出未绑定回调的单元控件并给出命中判定的补全建议。
在命中判定时拿点击坐标除以行高得到行索引,再结合列定义反查具体单元,就能定位到对应数据行。
先在表格控件层拦截双击并标记已处理,未命中的事件才上抛给图表层,避免两套逻辑同时触发。