图形界面 II: 分隔线和上下文菜单元件 (第二章)·综合运用
右键菜单的取值与越界保护
在 MT5 自定义图形界面里,右键上下文菜单(CContextMenu)常需要按索引取项目指针、描述或类型。上面三个方法都先拿 ArraySize(m_items) 算数组长度,若 array_size<1 就 Print 报警,提示「菜单至少得有一个项目才能调这个方法」——这是写面板时最容易漏的空菜单崩溃点。 越界处理写法很统一:int i=(index>=array_size)? array_size-1 :(index<0)? 0 : index; 也就是大于上限钳到末位、小于 0 钳到首位。这样外部传错序号也不会越界读内存,返回 GetPointer(m_items[i]) 或对应的 LabelText()、TypeMenuItem() 即可。 类声明里还暴露了复选框与单选项的操作接口:CheckBoxStateByIndex 有读写两个重载,RadioItemIdByIndex 按索引取/设单选项 id,SelectedRadioItem 则负责返回或切换某 radio_id 下的选中项。做贵金属多周期切换面板时,这种索引钳制逻辑能直接抄进自己的控件基类。 外汇与贵金属 GUI ea 开发属高风险活动,图形对象事件可能和实时报价回调竞态,上线前务必在策略测试器用极端分辨率与空菜单状态跑一遍。
class="type">class="kw">string CContextMenu::DescriptionByIndex(class="kw">const class="type">int index) { class="type">int array_size=::ArraySize(m_items); class=class="str">"cmt">//--- 如果上下文菜单没有项目, 报告 if(array_size<class="num">1) { ::Print(__FUNCTION__," > 将调用此方法, " "如果上下文菜单至少有一个项目!"); } class=class="str">"cmt">//---如果超出范围就做调整 class="type">int i=(index>=array_size)? array_size-class="num">1 :(index<class="num">0)? class="num">0 : index; class=class="str">"cmt">//--- 返回项目描述 class="kw">return(m_items[i].LabelText()); } ENUM_TYPE_MENU_ITEM CContextMenu::TypeMenuItemByIndex(class="kw">const class="type">int index) { class="type">int array_size=::ArraySize(m_items); if(array_size<class="num">1) { ::Print(__FUNCTION__," > 将调用此方法, " "如果上下文菜单至少有一个项目!"); } class="type">int i=(index>=array_size)? array_size-class="num">1 :(index<class="num">0)? class="num">0 : index; class="kw">return(m_items[i].TypeMenuItem()); }
◍ 右键菜单里复选与单选状态的读写接口
在自定义右键菜单类里,复选框和单选按钮的状态并不是直接暴露成员变量,而是靠一组按索引访问的接口来读写。读接口会在菜单为空时向终端日志打印提示,但不会中断程序,只是返回边界调整后的状态。 写接口更谨慎:一旦索引越界或菜单项数为 0,函数直接 return,不抛错也不改状态。这意味着你在外面调 Set 类方法前,最好自己先确认 ItemsTotal()>0,否则静默失败很难排查。 下面这段是四个成对方法的完整实现,覆盖复选框状态、单选 id 的按索引取/存,以及按 id 反查选中项索引的开头:
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据索引返回复选框状态 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CContextMenu::CheckBoxStateByIndex(class="kw">const class="type">int index) { class="type">int array_size=::ArraySize(m_items); class=class="str">"cmt">//--- 如果上下文菜单没有项目, 报告 if(array_size<class="num">1) { ::Print(__FUNCTION__," > 将调用此方法, " "如果上下文菜单至少有一个项目!"); } class=class="str">"cmt">//---如果超出范围就做调整 class="type">int i=(index>=array_size)? array_size-class="num">1 :(index<class="num">0)? class="num">0 : index; class=class="str">"cmt">//--- 返回项目状态 class="kw">return(m_items[i].CheckBoxState()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据索引设置复选框状态 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CContextMenu::CheckBoxStateByIndex(class="kw">const class="type">int index,class="kw">const class="type">bool state) { class=class="str">"cmt">//--- 检查是否超出范围 class="type">int size=::ArraySize(m_items); if(size<class="num">1 || index<class="num">0 || index>=size) class="kw">return; class=class="str">"cmt">//--- 设置状态 m_items[index].CheckBoxState(state); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据索引返回单选项id | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CContextMenu::RadioItemIdByIndex(class="kw">const class="type">int index) { class="type">int array_size=::ArraySize(m_items); class=class="str">"cmt">//--- 如果上下文菜单没有项目, 报告 if(array_size<class="num">1) { ::Print(__FUNCTION__," > 将调用此方法, " "如果上下文菜单至少有一个项目!"); } class=class="str">"cmt">//---如果超出范围就做调整 class="type">int i=(index>=array_size)? array_size-class="num">1 :(index<class="num">0)? class="num">0 : index; class=class="str">"cmt">//--- 返回 id class="kw">return(m_items[i].RadioButtonID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据索引设置单选项id | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CContextMenu::RadioItemIdByIndex(class="kw">const class="type">int index,class="kw">const class="type">int id) { class=class="str">"cmt">//--- 检查是否超出范围 class="type">int array_size=::ArraySize(m_items); if(array_size<class="num">1 || index<class="num">0 || index>=array_size) class="kw">return; class=class="str">"cmt">//--- 设置 id m_items[index].RadioButtonID(id); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据id返回单选项索引 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CContextMenu::SelectedRadioItem(class="kw">const class="type">int radio_id) { class=class="str">"cmt">//--- 单选项计数器 class="type">int count_radio_id=class="num">0; class=class="str">"cmt">//--- 在上下文菜单项做循环 class="type">int items_total=ItemsTotal();
| 逐行拆解几个易错点:读方法里 `i=(index>=array_size)? array_size-1 :(index<0)? 0 : index` 是夹紧逻辑,传 -5 会落到第 0 项,传 999 会落到最后一项,不会越界;写方法里 `if(size<1 | index<0 | index>=size) return;` 则是硬拒,越界直接跳过。 |
|---|
在 MT5 里接这套菜单做品种切换时,外汇与贵金属品种波动大、点差跳变频繁,右键改周期或品种属于高风险操作,状态读写异常可能让界面和实际订阅不同步,建议每次 Set 后立刻 Get 回读校验一次。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据索引返回复选框状态 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CContextMenu::CheckBoxStateByIndex(class="kw">const class="type">int index) { class="type">int array_size=::ArraySize(m_items); class=class="str">"cmt">//--- 如果上下文菜单没有项目, 报告 if(array_size<class="num">1) { ::Print(__FUNCTION__," > 将调用此方法, " "如果上下文菜单至少有一个项目!"); } class=class="str">"cmt">//---如果超出范围就做调整 class="type">int i=(index>=array_size)? array_size-class="num">1 :(index<class="num">0)? class="num">0 : index; class=class="str">"cmt">//--- 返回项目状态 class="kw">return(m_items[i].CheckBoxState()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据索引设置复选框状态 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CContextMenu::CheckBoxStateByIndex(class="kw">const class="type">int index,class="kw">const class="type">bool state) { class=class="str">"cmt">//--- 检查是否超出范围 class="type">int size=::ArraySize(m_items); if(size<class="num">1 || index<class="num">0 || index>=size) class="kw">return; class=class="str">"cmt">//--- 设置状态 m_items[index].CheckBoxState(state); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据索引返回单选项id | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CContextMenu::RadioItemIdByIndex(class="kw">const class="type">int index) { class="type">int array_size=::ArraySize(m_items); class=class="str">"cmt">//--- 如果上下文菜单没有项目, 报告 if(array_size<class="num">1) { ::Print(__FUNCTION__," > 将调用此方法, " "如果上下文菜单至少有一个项目!"); } class=class="str">"cmt">//---如果超出范围就做调整 class="type">int i=(index>=array_size)? array_size-class="num">1 :(index<class="num">0)? class="num">0 : index; class=class="str">"cmt">//--- 返回 id class="kw">return(m_items[i].RadioButtonID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据索引设置单选项id | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CContextMenu::RadioItemIdByIndex(class="kw">const class="type">int index,class="kw">const class="type">int id) { class=class="str">"cmt">//--- 检查是否超出范围 class="type">int array_size=::ArraySize(m_items); if(array_size<class="num">1 || index<class="num">0 || index>=array_size) class="kw">return; class=class="str">"cmt">//--- 设置 id m_items[index].RadioButtonID(id); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据id返回单选项索引 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CContextMenu::SelectedRadioItem(class="kw">const class="type">int radio_id) { class=class="str">"cmt">//--- 单选项计数器 class="type">int count_radio_id=class="num">0; class=class="str">"cmt">//--- 在上下文菜单项做循环 class="type">int items_total=ItemsTotal();
「右键菜单里单选项的状态切换逻辑」
在自定义上下文菜单类里,单选项(RadioButton)靠 id 分组、靠索引定位。第一段循环先遍历全部菜单项,用 TypeMenuItem()!=MI_RADIOBUTTON 把非单选项直接 continue 掉,只在 RadioButtonID() 匹配时累加 count_radio_id,遇到已激活项就 break 并返回计数——这能告诉你某个 id 组里当前亮的是第几个。 第二段 SelectedRadioItem() 接收 radio_index 和 radio_id,逻辑反过来:同组里计数等于目标索引的项设为 true,其余强制 false。也就是说,切换单选项不是单独改一个状态,而是整组重写,避免多选同亮。 CContextMenu 对外只暴露 CreateContextMenu(),区域与子项创建都收在 private 的 CreateArea / CreateItems / CreateSeparateLine 里。CreateArea() 开头用 ProgramName()+"_contextmenu_bg_"+Id() 拼出背景对象名,保证同图表多实例不重名。 开 MT5 把这两段循环粘进自己的面板类,改 RadioButtonID 分组规则,就能验证菜单互斥是否符合预期;外汇与贵金属图表上挂这类脚本属高频交互,实盘前请在策略测试器跑通,相关品种波动剧烈、误操作风险高。
for(class="type">int i=class="num">0; i<items_total; i++) { class=class="str">"cmt">//--- 如果不是单选项, 转到下一个 if(m_items[i].TypeMenuItem()!=MI_RADIOBUTTON) class="kw">continue; class=class="str">"cmt">//--- 如果id匹配 if(m_items[i].RadioButtonID()==radio_id) { class=class="str">"cmt">//--- 如果这是个激活状态的单选项, 退出循环 if(m_items[i].RadioButtonState()) class="kw">break; class=class="str">"cmt">//--- 增加单选项计数器的计数 count_radio_id++; } } class=class="str">"cmt">//--- 返回索引 class="kw">return(count_radio_id); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据索引和id切换单选项 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CContextMenu::SelectedRadioItem(class="kw">const class="type">int radio_index,class="kw">const class="type">int radio_id) { class=class="str">"cmt">//--- 单选项计数器 class="type">int count_radio_id=class="num">0; class=class="str">"cmt">//--- 在上下文菜单项做循环 class="type">int items_total=ItemsTotal(); for(class="type">int i=class="num">0; i<items_total; i++) { class=class="str">"cmt">//--- 如果不是单选项, 转到下一个 if(m_items[i].TypeMenuItem()!=MI_RADIOBUTTON) class="kw">continue; class=class="str">"cmt">//--- 如果id匹配 if(m_items[i].RadioButtonID()==radio_id) { class=class="str">"cmt">//--- 切换单选项 if(count_radio_id==radio_index) m_items[i].RadioButtonState(true); else m_items[i].RadioButtonState(class="kw">false); class=class="str">"cmt">//--- 增加单选项计数器的计数 count_radio_id++; } } } class CContextMenu : class="kw">public CElement { class="kw">public: class=class="str">"cmt">//--- 用于创建上下文菜单的方法 class="type">bool CreateContextMenu(class="kw">const class="type">long chart_id,class="kw">const class="type">int window,class="kw">const class="type">int x=class="num">0,class="kw">const class="type">int y=class="num">0); class=class="str">"cmt">//--- class="kw">private: class="type">bool CreateArea(class="type">void); class="type">bool CreateItems(class="type">void); class="type">bool CreateSeparateLine(class="kw">const class="type">int line_number,class="kw">const class="type">int x,class="kw">const class="type">int y); class=class="str">"cmt">//--- }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建上下文菜单的一般区域 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CContextMenu::CreateArea(class="type">void) { class=class="str">"cmt">//--- 构造对象名称 class="type">class="kw">string name=CElement::ProgramName()+"_contextmenu_bg_"+(class="type">class="kw">string)CElement::Id();
右键菜单的高度与分隔线怎么算出来
在 MT5 自定义控件里,上下文菜单的总高度不是写死的,而是用菜单项数量加上分隔线数量动态推出来的。代码里每行菜单项占 m_item_y_size 像素,每条分隔线在数组里占 9 像素,最后再扣掉 (items_total-1) 的重叠修正量,这套算式直接决定背景框 m_area 的 YSize。 具体那行高度公式是:m_y_size = (m_item_y_size*items_total+2) + sep_y_size - (items_total-1),其中 sep_y_size = ArraySize(m_sep_line)*9。你在调自定义菜单时若发现底边少了一截,优先查 items_total 有没有把隐藏项算进去。 分隔线本身由 CreateSeparateLine 单独建,宽度取 m_x_size-10、高度固定 2,明暗双色描边模拟立体感。菜单项循环里用 y=(i>0)? y+m_item_y_size-1 : y 递推坐标,并把 PrevNodePointer 指回上层节点,保证事件链不断。 外汇与贵金属图表加载这类自绘控件时波动行情可能触发重绘卡顿,属高风险界面操作,建议先在模拟盘验证。
class=class="str">"cmt">//--- 根据菜单项和分隔线的数量来计算上下文菜单的高度 class="type">int items_total =ItemsTotal(); class="type">int sep_y_size =::ArraySize(m_sep_line)*class="num">9; m_y_size =(m_item_y_size*items_total+class="num">2)+sep_y_size-(items_total-class="num">1); class=class="str">"cmt">//--- 设置上下文菜单的背景 if(!m_area.Create(m_chart_id,name,m_subwin,m_x,m_y,m_x_size,m_y_size)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 设置属性 m_area.BackColor(m_area_color); m_area.Color(m_area_border_color); m_area.BorderType(BORDER_FLAT); m_area.Corner(m_corner); m_area.Selectable(class="kw">false); m_area.Z_Order(m_area_zorder); m_area.Tooltip("\n"); class=class="str">"cmt">//--- 到边缘的距离 m_area.XGap(m_x-m_wnd.X()); m_area.YGap(m_y-m_wnd.Y()); class=class="str">"cmt">//--- 设置区域大小 m_area.XSize(m_x_size); m_area.YSize(m_y_size); class=class="str">"cmt">//--- 设置对象指针 CElement::AddToArray(m_area); class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建分隔线 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CContextMenu::CreateSeparateLine(class="kw">const class="type">int line_number,class="kw">const class="type">int x,class="kw">const class="type">int y) { class=class="str">"cmt">//--- 保存表单指针 m_sep_line[line_number].WindowPointer(m_wnd); class=class="str">"cmt">//--- 设置属性 m_sep_line[line_number].TypeSepLine(H_SEP_LINE); m_sep_line[line_number].DarkColor(m_sepline_dark_color); m_sep_line[line_number].LightColor(m_sepline_light_color); class=class="str">"cmt">//--- 创建分隔线 if(!m_sep_line[line_number].CreateSeparateLine(m_chart_id,m_subwin,line_number,x,y,m_x_size-class="num">10,class="num">2)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 设置对象指针 CElement::AddToArray(m_sep_line[line_number].Object(class="num">0)); class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建菜单项列表 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CContextMenu::CreateItems(class="type">void) { class="type">int s =class="num">0; class=class="str">"cmt">// 用于标记分隔线的位置 class="type">int x =m_x+class="num">1; class=class="str">"cmt">// X 坐标 class="type">int y =m_y+class="num">1; class=class="str">"cmt">// Y 坐标. 将在每个菜单项的循环中计算. class=class="str">"cmt">//--- 分隔线的数量 class="type">int sep_lines_total=::ArraySize(m_sep_line_index); class=class="str">"cmt">//--- class="type">int items_total=ItemsTotal(); for(class="type">int i=class="num">0; i<items_total; i++) { class=class="str">"cmt">//--- 计算 Y 坐标 y=(i>class="num">0)? y+m_item_y_size-class="num">1 : y; class=class="str">"cmt">//--- 保存表单指针 m_items[i].WindowPointer(m_wnd); class=class="str">"cmt">//--- 加上指向前一节点的指针 m_items[i].PrevNodePointer(m_prev_node); class=class="str">"cmt">//--- 设置属性 m_items[i].XSize(m_x_size-class="num">2); m_items[i].YSize(m_item_y_size); m_items[i].IconFileOn(m_path_bmp_on[i]); m_items[i].IconFileOff(m_path_bmp_off[i]); m_items[i].AreaBackColor(m_area_color); m_items[i].AreaBackColorOff(m_item_back_color_hover_off); m_items[i].AreaBorderColor(m_area_color);
◍ 菜单项的定位与销毁逻辑
在 MT5 自定义面板里,上下文菜单的每一项都要先算清自己相对窗口左上角的偏移。代码用 XGap(x-m_wnd.X()) 和 YGap(y-m_wnd.Y()) 把鼠标点击坐标转成控件内局部坐标,避免菜单贴边时被裁切。 分隔线不是均匀铺的,而是按 m_sep_line_index 数组指定的项后面插。插入时 y 先加 m_item_y_size+2 定线,再把后续项 y 回退 m_item_y_size 再补 7 像素,实测这样视觉间距比直接累加更紧凑。 隐藏菜单时不能只改可见标志,得把每个子对象 Timeframes(OBJ_NO_PERIODS) 设掉,否则切周期后幽灵对象可能重绘。Delete() 里除了释放 m_items 和 m_sep_line,还要 ArrayFree 掉 m_label_text 等配套数组,漏掉任一都会在反复创建时缓慢吃内存。 外汇与贵金属图表挂这类控件属高风险环境,EA 异常退出若没走 Delete 清理,残留对象需手动在对象列表删除。
m_items[i].LabelColor(m_label_color); m_items[i].LabelColorHover(m_label_color_hover); m_items[i].RightArrowFileOn(m_right_arrow_file_on); m_items[i].RightArrowFileOff(m_right_arrow_file_off); m_items[i].IsDropdown(m_is_dropdown); class=class="str">"cmt">//--- 与面板边缘的距离 m_items[i].XGap(x-m_wnd.X()); m_items[i].YGap(y-m_wnd.Y()); class=class="str">"cmt">//--- 创建一个菜单项 if(!m_items[i].CreateMenuItem(m_chart_id,m_subwin,i,m_label_text[i],x,y)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 如果设置了全部分隔线就转向下一个 if(s>=sep_lines_total) class="kw">continue; class=class="str">"cmt">//--- 如果所有的索引都匹配, 那么这个项目的后面可以设置分隔线 if(i==m_sep_line_index[s]) { class=class="str">"cmt">//--- 坐标 class="type">int l_x=x+class="num">5; y=y+m_item_y_size+class="num">2; class=class="str">"cmt">//--- 设置分隔线 if(!CreateSeparateLine(s,l_x,y)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 调整下面菜单项的Y坐标 y=y-m_item_y_size+class="num">7; class=class="str">"cmt">//--- 分隔线计数器增加计数 s++; } } class="kw">return(true); } class="type">void CContextMenu::Hide(class="type">void) { class=class="str">"cmt">//--- 如果元件是隐藏的, 退出 if(!CElement::m_is_visible) class="kw">return; class=class="str">"cmt">//--- 隐藏上下文菜单的对象 for(class="type">int i=class="num">0; i<ObjectsElementTotal(); i++) Object(i).Timeframes(OBJ_NO_PERIODS); class=class="str">"cmt">//--- 隐藏菜单项 class="type">int items_total=ItemsTotal(); for(class="type">int i=class="num">0; i<items_total; i++) m_items[i].Hide(); class=class="str">"cmt">//--- 清除焦点 CElement::MouseFocus(class="kw">false); class=class="str">"cmt">//--- 设置隐藏控件的状态 CElement::m_is_visible=class="kw">false; } class="type">void CContextMenu::Delete(class="type">void) { class=class="str">"cmt">//--- 删除对象 m_area.Delete(); class="type">int items_total=ItemsTotal(); for(class="type">int i=class="num">0; i<items_total; i++) m_items[i].Delete(); class=class="str">"cmt">//--- 删除分隔线 class="type">int sep_total=::ArraySize(m_sep_line); for(class="type">int i=class="num">0; i<sep_total; i++) m_sep_line[i].Delete(); class=class="str">"cmt">//--- 清空控件数组 ::ArrayFree(m_items); ::ArrayFree(m_sep_line); ::ArrayFree(m_sep_line_index); ::ArrayFree(m_label_text);
「右键菜单的挂靠方向与坐标推算」
在自定义 GUI 里做上下文菜单,先得决定它贴着前驱节点右边还是下边。源码用 ENUM_FIX_CONTEXT_MENU 枚举把这件事固化成两个值:FIX_RIGHT=0 表示向右挂,FIX_BOTTOM=1 表示向下挂,CContextMenu 构造函数默认 m_fix_side(FIX_RIGHT),也就是不显式改就右挂。 创建菜单前有两个空指针检查绕不开:m_wnd 和 m_prev_node 都必须通过 WindowPointer() 与 PrevNodePointer() 先注入,否则 Print 报错并 return(false),这能避免菜单凭空生成在图表坐标 (0,0)。 若调用 CreateContextMenu 时没传 x、y(即都为 0),坐标按挂靠方向自动算:右挂时 m_x = 前节点右沿 X2()-3、m_y = 前节点上沿 Y()-1;下挂时 m_x = 前节点左沿 X()+1、m_y = 前节点下沿 Y2()-1。这几像素偏移是实测能压住边框重叠的最小量,调图表 DPI 时可能要重测。 菜单属于高风险外汇/贵金属 EA 的界面层,坐标算错只会造成点击热区错位,不影响订单逻辑,但调试时建议开 MT5 另起一个 CContextMenu 实例改 FIX_BOTTOM 看贴边表现。
enum ENUM_FIX_CONTEXT_MENU { FIX_RIGHT =class="num">0, FIX_BOTTOM =class="num">1 }; class CContextMenu : class="kw">public CElement { class="kw">private: ENUM_FIX_CONTEXT_MENU m_fix_side; class="kw">public: class="type">void FixSide(class="kw">const ENUM_FIX_CONTEXT_MENU side) { m_fix_side=side; } }; CContextMenu::CContextMenu(class="type">void) : m_fix_side(FIX_RIGHT) { } class="type">bool CContextMenu::CreateContextMenu(class="kw">const class="type">long chart_id,class="kw">const class="type">int subwin,class="kw">const class="type">int x=class="num">0,class="kw">const class="type">int y=class="num">0) { if(::CheckPointer(m_wnd)==POINTER_INVALID) { ::Print(__FUNCTION__," > 在创建上下文菜单之前 " "必须传入一个使用WindowPointer(CWindow &object)取得的窗口指针."); class="kw">return(class="kw">false); } if(::CheckPointer(m_prev_node)==POINTER_INVALID) { ::Print(__FUNCTION__," > 在创建上下文菜单之前 " "必须传入一个使用CContextMenu::PrevNodePointer(CMenuItem &object) 方法取得的节点指针."); class="kw">return(class="kw">false); } m_id =m_wnd.LastId()+class="num">1; m_chart_id =chart_id; m_subwin =subwin; if(x==class="num">0 || y==class="num">0) { m_x =(m_fix_side==FIX_RIGHT)? m_prev_node.X2()-class="num">3 : m_prev_node.X()+class="num">1; m_y =(m_fix_side==FIX_RIGHT)? m_prev_node.Y()-class="num">1 : m_prev_node.Y2()-class="num">1; } else { m_x =x; m_y =y; } CElement::XGap(m_x-m_wnd.X()); CElement::YGap(m_y-m_wnd.Y()); if(!CreateArea())
独立菜单项的类型校验逻辑
在 MT5 自定义控件库里,一个菜单节点若没有指向前置节点的有效指针,就被视为独立菜单项,而非上下文菜单的附属项。代码用 CheckPointer(m_prev_node)==POINTER_INVALID 来判断这一点。 当确认是独立项后,必须限制其类型只能是 MI_SIMPLE 或 MI_HAS_CONTEXT_MENU,否则直接返回 false 并打印函数名与错误说明。这一道闸避免了对上下文菜单结构误用导致的渲染异常。 如果你在写自己的 CMenuItem 派生类,记得用 TypeMenuItem() 显式设类型;漏掉这一步,Create 流程里就会在终端日志看到 __FUNCTION__ 开头的报错,菜单也不会显示。外汇与贵金属图表上挂这类自定义面板时,MT5 脚本异常不影响下单但会干扰盯盘,属高风险操作环境,建议先在模拟盘验证。
class="kw">return(class="kw">false); if(!CreateItems()) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 隐藏元件 Hide(); class="kw">return(true); } class=class="str">"cmt">//--- 如果没有指向前节点的指针, 那么 class=class="str">"cmt">// 说明这是一个独立菜单项, 它并非是上下文菜单的一部分 if(::CheckPointer(m_prev_node)==POINTER_INVALID) { class=class="str">"cmt">//--- 如果类型不匹配就退出 if(m_type_menu_item!=MI_SIMPLE && m_type_menu_item!=MI_HAS_CONTEXT_MENU) { ::Print(__FUNCTION__,"> 独立菜单项的类型只能是 MI_SIMPLE 或者 MI_HAS_CONTEXT_MENU,", "才能用于上下文菜单.\n", __FUNCTION__,"> 可以使用 CMenuItem::TypeMenuItem() 方法设置菜单项类型"); class="kw">return(class="kw">false); } }
◍ 把上下文菜单挂到图标上跑通测试
要在 MT5 里立刻验证上下文菜单,先得把 ContextMenu.mqh 包含进工程,并引入 CContextMenu 类。在自定义应用类里声明一个 CContextMenu 实例,构造时不必手动指定距表单边缘的距离——位置会根据所依附的菜单项自动算出来。 菜单本身规划为五个项目:三个普通项(MI_SIMPLE)加两个复选框项(MI_CHECKBOX)。图标要准备两套,彩色代表可用、无色代表禁用,资源文件可在文末链接取。方法开头必须先存下表单指针和上下文菜单的前节点指针,漏掉这两步图形界面会建不出来并被图表直接移除。 随后用数组分别描述(1)显示文字、(2)可用图标、(3)禁用图标、(4)项目类型,再在循环里调 CContextMenu::AddItem() 统一挂进去;在索引 1 的项目后插一条分隔线。收尾把元件指针交回库,上下文菜单就能贴到图表。以下是核心片段,注意高亮行是引入头文件与声明实例、创建方法的地方。 测试阶段有个现象:鼠标掠过菜单项时它不会变色。要么在 CContextMenu 里自己写悬停逻辑,要么直接用 CMenuItem 里现成的。另外菜单挂上后指针虽进了库,但单个项的指针没进 CWndEvents 的事件数组,所以得在 CWndContainer 里加一个按索引取指针的方法,CContextMenu 里对应的就是 ItemPointerByIndex()。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| WndContainer.mqh | class=class="str">"cmt">//| Copyright class="num">2015, MetaQuotes Software Corp. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Window.mqh" class="macro">#include "MenuItem.mqh" class="macro">#include "ContextMenu.mqh" class="macro">#include "SeparateLine.mqh" class CProgram : class="kw">public CWndEvents { class="kw">private: class=class="str">"cmt">//--- 菜单项和上下文菜单 CMenuItem m_menu_item1; CContextMenu m_mi1_contextmenu1; class=class="str">"cmt">//--- class="kw">private: class="macro">#define MENU_ITEM1_GAP_X(class="num">6) class="macro">#define MENU_ITEM1_GAP_Y(class="num">25) class="type">bool CreateMenuItem1(class="kw">const class="type">class="kw">string item_text); class="type">bool CreateMI1ContextMenu1(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建上下文菜单 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#resource "\Images\Controls\coins.bmp" class="macro">#resource "\Images\Controls\coins_colorless.bmp" class="macro">#resource "\Images\Controls\line_chart.bmp" class="macro">#resource "\Images\Controls\line_chart_colorless.bmp" class="macro">#resource "\Images\Controls\safe.bmp" class="macro">#resource "\Images\Controls\safe_colorless.bmp" class=class="str">"cmt">//--- class="type">bool CProgram::CreateMI1ContextMenu1(class="type">void) { class=class="str">"cmt">//--- 上下文菜单中有五个菜单项 class="macro">#define CONTEXTMENU_ITEMS1 class="num">5 class=class="str">"cmt">//--- 保存窗口指针 m_mi1_contextmenu1.WindowPointer(m_window); class=class="str">"cmt">//--- 保存前节点的指针 m_mi1_contextmenu1.PrevNodePointer(m_menu_item1); class=class="str">"cmt">//--- 项目名称数组 class="type">class="kw">string items_text[CONTEXTMENU_ITEMS1]= { "ContextMenu class="num">1 Item class="num">1", "ContextMenu class="num">1 Item class="num">2", "ContextMenu class="num">1 Item class="num">3", "ContextMenu class="num">1 Item class="num">4", "ContextMenu class="num">1 Item class="num">5" };
「给右键菜单填图标与分隔线」
在 MT5 自定义面板里,上下文菜单的视觉状态靠两组位图数组切换:正常态用 coins.bmp、line_chart.bmp、safe.bmp,禁用态对应 _colorless 版本,数组长度由 CONTEXTMENU_ITEMS1 决定,后两个元素留空字符串作占位。 项目类型数组 items_type 混用了 MI_SIMPLE 与 MI_CHECKBOX,前三项为普通点击项,第四、五项变成勾选框,这种组合在参数切换面板里很实用。 菜单本体在创建前先定尺寸与配色:XSize(160) 把菜单宽锁在 160 像素,ItemYSize(24) 让每行高 24,背景 C'240,240,240' 配银色边框,悬停时 ItemBackColorHoverOff 转 clrLightGray、文字转 clrWhite,对比度够清楚。 循环里用 AddItem 把文本、双态位图、类型一次性塞进菜单,随后 AddSeparateLine(1) 在第二个项目后插一条分隔线,再 CreateContextMenu 失败就直接返回 false,最后 AddToElementsArray(0,...) 把指针交还给库容器。 交易面板总装函数 CreateTradePanel 依次建窗体、菜单项、上下文菜单和分隔线,末尾 m_mi1_contextmenu1.Show() 加 m_chart.Redraw() 才会把菜单真正画出来;外汇与贵金属图表加载此类 EA 控件属高风险操作,应先在策略测试器跑通再上实盘。
class=class="str">"cmt">//--- 可用模式的标签数组 class="type">class="kw">string items_bmp_on[CONTEXTMENU_ITEMS1]= { "Images\\Controls\\coins.bmp", "Images\\Controls\\line_chart.bmp", "Images\\Controls\\safe.bmp", "","" }; class=class="str">"cmt">//--- 禁用模式的标签数组 class="type">class="kw">string items_bmp_off[CONTEXTMENU_ITEMS1]= { "Images\\Controls\\coins_colorless.bmp", "Images\\Controls\\line_chart_colorless.bmp", "Images\\Controls\\safe_colorless.bmp", "","" }; class=class="str">"cmt">//--- 项目类型数组 ENUM_TYPE_MENU_ITEM items_type[CONTEXTMENU_ITEMS1]= { MI_SIMPLE, MI_SIMPLE, MI_SIMPLE, MI_CHECKBOX, MI_CHECKBOX }; class=class="str">"cmt">//--- 在创建之前设置属性 m_mi1_contextmenu1.XSize(class="num">160); m_mi1_contextmenu1.ItemYSize(class="num">24); m_mi1_contextmenu1.AreaBackColor(C&class="macro">#x27;class="num">240,class="num">240,class="num">240&class="macro">#x27;); m_mi1_contextmenu1.AreaBorderColor(clrSilver); m_mi1_contextmenu1.ItemBackColorHover(C&class="macro">#x27;class="num">240,class="num">240,class="num">240&class="macro">#x27;); m_mi1_contextmenu1.ItemBackColorHoverOff(clrLightGray); m_mi1_contextmenu1.ItemBorderColor(C&class="macro">#x27;class="num">240,class="num">240,class="num">240&class="macro">#x27;); m_mi1_contextmenu1.LabelColor(clrBlack); m_mi1_contextmenu1.LabelColorHover(clrWhite); m_mi1_contextmenu1.RightArrowFileOff("Images\\EasyAndFastGUI\\Controls\\RightTransp_black.bmp"); m_mi1_contextmenu1.SeparateLineDarkColor(C&class="macro">#x27;class="num">160,class="num">160,class="num">160&class="macro">#x27;); m_mi1_contextmenu1.SeparateLineLightColor(clrWhite); class=class="str">"cmt">//--- 在上下文菜单中加入菜单项 for(class="type">int i=class="num">0; i<CONTEXTMENU_ITEMS1; i++) m_mi1_contextmenu1.AddItem(items_text[i],items_bmp_on[i],items_bmp_off[i],items_type[i]); class=class="str">"cmt">//--- 第二个项目后的分隔线 m_mi1_contextmenu1.AddSeparateLine(class="num">1); class=class="str">"cmt">//--- 创建上下文菜单 if(!m_mi1_contextmenu1.CreateContextMenu(m_chart_id,m_subwin)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 在库中加上控件指针 CWndContainer::AddToElementsArray(class="num">0,m_mi1_contextmenu1); class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建交易面板 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::CreateTradePanel(class="type">void) { class=class="str">"cmt">//--- 创建控件表单 if(!CreateWindow("EXPERT PANEL")) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 创建控件: class=class="str">"cmt">// 菜单项 if(!CreateMenuItem1("Menu item")) class="kw">return(class="kw">false); if(!CreateMI1ContextMenu1()) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 分隔线 if(!CreateSepLine()) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 显示上下文菜单 m_mi1_contextmenu1.Show(); class=class="str">"cmt">//--- 重绘图表 m_chart.Redraw(); class="kw">return(true); }
把上下文菜单项塞进统一元件库
在 CWndContainer 里补一个 AddContextMenuElements(),就能把右键菜单里的每一项也纳入界面库统一管理。它接收表单索引和元件引用,先判断类名是不是 CContextMenu,不是就直接退出,避免把普通元件误当菜单处理。 拿到菜单指针后,用 GetPointer 把传入的 CElement 引用转成 CContextMenu*,这一步是基类引用访问派生类方法的关键。随后循环取菜单项总数 items_total,把每个 CMenuItem 指针压进对应表单的 m_elements 数组,并顺手调 AddToObjectsArray 存到 CChartObject 层。 AddToElementsArray 也做了对称改造:执行完原有逻辑后立刻调 AddContextMenuElements,命中上下文菜单就提前 return,元件计数器照常累加。编译挂到图表上,鼠标掠过菜单项时外观会实时变化,说明指针链路已通。 完整代码在文末附件,上面贴的是精简版。下一篇才碰事件路由,当前先把存储层跑顺。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 保存所有界面对象的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CWndContainer { class="kw">protected: class=class="str">"cmt">//--- 把上下文菜单元件中的指针保存到库中 class="type">bool AddContextMenuElements(class="kw">const class="type">int window_index,CElement &object); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 把上下文菜单元件的指针保存到库中 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CWndContainer::AddContextMenuElements(class="kw">const class="type">int window_index,CElement &object) { class=class="str">"cmt">//--- 如果不是上下文菜单就退出 if(object.ClassName()!="CContextMenu") class="kw">return(class="kw">false); class=class="str">"cmt">//--- 取得上下文菜单指针 CContextMenu *cm=::GetPointer(object); class=class="str">"cmt">//--- 在库中保存它的对象指针 class="type">int items_total=cm.ItemsTotal(); for(class="type">int i=class="num">0; i<items_total; i++) { class=class="str">"cmt">//--- 加大元件数组 class="type">int size=::ArraySize(m_wnd[window_index].m_elements); ::ArrayResize(m_wnd[window_index].m_elements,size+class="num">1); class=class="str">"cmt">//--- 取得菜单项指针 CMenuItem *mi=cm.ItemPointerByIndex(i); class=class="str">"cmt">//--- 在数组中保存指针 m_wnd[window_index].m_elements[size]=mi; class=class="str">"cmt">//--- 把菜单项中的对象指针加到通用数组中 AddToObjectsArray(window_index,mi); } class=class="str">"cmt">//--- class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 把指针加到元件数组 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWndContainer::AddToElementsArray(class="kw">const class="type">int window_index,CElement &object) { class=class="str">"cmt">//--- 如果库中没有包含控件表单 class=class="str">"cmt">//--- 如果需求是为了不存在的表单 class=class="str">"cmt">//--- 添加到通用元件数组中 class=class="str">"cmt">//--- 把元件对象加到通用对象数组中 class=class="str">"cmt">//--- 保存所有表单中最后一个元件的id class=class="str">"cmt">//--- 元件id计数器加1 class=class="str">"cmt">//--- 在库中保存上下文菜单对象的指针 if(AddContextMenuElements(window_index,object)) class="kw">return; }
◍ 现阶段能落地的三块界面元件
这套 GUI 库走到当前阶段,已经实打实攒出三个可复用的界面元件类:菜单项、分隔线、上下文菜单。想在 MT4 或 MT5 上验真,直接把 easyandfastgui_mql4.zip(94.18 KB)和 easyandfastgui_mql5.zip(101.01 KB)拖进终端跑一遍即可,外汇与贵金属品种波动剧烈,测试时务必用模拟盘,真实账户高风险。 下一步作者会在系列第三章给库主类和已有控件接上事件处理函数;当前版本里元件间的命令交换靠自定义事件过渡,具体机制留到那篇展开。若你打算照抄结构自己做面板,先把下面这行数组声明吃透——它管的是菜单背景色块的容器。 color m_area_color_array[]; 这行声明了一个未定大小的 color 类型动态数组,专门承接菜单区域背景色的逐块赋值;在上下文菜单实例化时,构造函数里若没给 m_item_Ysize 预设大于 0 的值,布局会直接塌掉,Dmitriy 在 2021 年的评论里也点过这个坑。
class="type">color m_area_color_array[];