图形界面 II: 分隔线和上下文菜单元件 (第二章)·综合运用
📘

图形界面 II: 分隔线和上下文菜单元件 (第二章)·综合运用

第 3/3 篇

右键菜单的取值与越界保护

在 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 开发属高风险活动,图形对象事件可能和实时报价回调竞态,上线前务必在策略测试器用极端分辨率与空菜单状态跑一遍。

MQL5 / C++
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 反查选中项索引的开头:

MQL5 / C++
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<1index<0index>=size) return;` 则是硬拒,越界直接跳过。

在 MT5 里接这套菜单做品种切换时,外汇与贵金属品种波动大、点差跳变频繁,右键改周期或品种属于高风险操作,状态读写异常可能让界面和实际订阅不同步,建议每次 Set 后立刻 Get 回读校验一次。

MQL5 / C++
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 分组规则,就能验证菜单互斥是否符合预期;外汇与贵金属图表上挂这类脚本属高频交互,实盘前请在策略测试器跑通,相关品种波动剧烈、误操作风险高。

MQL5 / C++
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 指回上层节点,保证事件链不断。 外汇与贵金属图表加载这类自绘控件时波动行情可能触发重绘卡顿,属高风险界面操作,建议先在模拟盘验证。

MQL5 / C++
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 清理,残留对象需手动在对象列表删除。

MQL5 / C++
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 看贴边表现。

MQL5 / C++
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 脚本异常不影响下单但会干扰盯盘,属高风险操作环境,建议先在模拟盘验证。

MQL5 / C++
   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()。

MQL5 / C++
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 控件属高风险操作,应先在策略测试器跑通再上实盘。

MQL5 / C++
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,元件计数器照常累加。编译挂到图表上,鼠标掠过菜单项时外观会实时变化,说明指针链路已通。 完整代码在文末附件,上面贴的是精简版。下一篇才碰事件路由,当前先把存储层跑顺。

MQL5 / C++
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 年的评论里也点过这个坑。

MQL5 / C++
class="type">color                m_area_color_array[];

常见问题

用菜单暴露的读写接口取 Checked 属性,非零即选中;拿到值后写进品种对应的 ini 字段,下次加载时回填即可。
在单选组维护一个当前索引变量,点击新项时先清掉旧项选中标记再置新项,避免多选中。
可以,小布能按你贴的界面逻辑跑一遍坐标与挂靠方向推算,标出可能越出画布边界的菜单项。
高度=表项高×项数+分隔线高×分隔数;漏算隐藏项或动态插入的分隔线就会多出空白段。
先查销毁时是否按创建顺序逆序释放对象,再核对挂靠坐标在缩放后是否未同步更新。