图形界面 X: 排序、重建表格和单元格中的控件 (集成编译 11)·综合运用
「用计数器捕捉图表双击」
在 MT5 自定义指标或 EA 里,想区分用户是单击还是双击图表,可以靠两次按下事件的时间差来判断。这里用 curr_depressed - prev_depressed 算出间隔计数,再和预设的 m_pause_between_clicks 阈值比较。
若间隔小于阈值,就通过 EventChartCustom 向图表抛出 ON_DOUBLE_CLICK 自定义事件,把计数器作为 lparam 传出去,方便后续逻辑处理。外汇与贵金属行情跳动快,这类交互检测建议先在模拟盘验证,误触可能倾向引发非预期下单。
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 策略测试器里先验证点击事件投递是否符合预期。
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,点按钮格看是否触发而你点空白格无反应——外汇和贵金属图表上做这种交互面板属高风险环境,参数没隔离好容易误触下单逻辑。
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 索引做状态切换。外汇与贵金属图表挂这类交互表格属高风险操作环境,建议先在策略测试器里用模拟点击验证事件触发。
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 工程核对行为:
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,双击表格单元格就能稳定触发回调,而不会和拖动、单击事件打架。外汇与贵金属图表波动剧烈,这类交互延迟虽小,但高频操作下仍可能错失价位。
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 把菜单段取消注释对比行为差异。外汇与贵金属品种波动剧烈,拿这类界面做批量下单前务必先在策略测试器跑通事件响应。
<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>) { <span class="comment">class=class="str">"cmt">//--- 为控件创建表格 class="num">1</span> <span class="keyword">if</span>(!CreateWindow1(<span class="class="type">class="kw">string">"EXPERT PANEL"</span>)) <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> <span class="keyword">if</span>(!CreateStatusBar(<span class="number">class="num">1</span>,<span class="number">class="num">25</span>)) <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>); <span class="comment">class=class="str">"cmt">//--- 按钮</span> <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>)) <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>); <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>)) <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>); <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>)) <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>); <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>)) <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>); <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>)) <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>); <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>)) <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>); <span class="comment">class=class="str">"cmt">//--- 编辑</span> <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>)) <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>); <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>)) <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>); <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>)) <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>); <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>)) <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 行在你的分辨率下会不会溢出边框。
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); }