图形界面 II: 菜单项元件 (第一章)·综合运用
(3/3)· 把前 two 篇的界面骨架真正用起来,用 7 节拆开菜单项类与下拉交互的最后一公里
「菜单项悬停变色与焦点判定」
在 MT5 自定义控件里,菜单项(CMenuItem)的视觉反馈靠鼠标焦点驱动。悬停时改图标、背景、文字三处颜色,离开即还原,这是 GUI 交互的基本盘。 ChangeObjectsColor() 先拦截一种特例:若该项带上下文菜单且正处于展开状态(m_context_menu_state 为真),直接 return,不抢焦点色。否则按类型分流——简单项/含菜单项走一套逻辑,勾选框/单选钮走另一套。 对于简单项,有焦点时若 m_item_state 为真,图标和箭头都切到 hover 态,背景与标签色取 m_area_color_hover / m_label_color_hover;无焦点则箭头收起、背景退回 m_area_color。勾选类只认鼠标是否悬停,不依赖 item_state,逻辑更短。 ResetColors() 是强制清零:把 MouseFocus 置 false,背景和标签色写回默认成员值,相当于外部调用时一键褪色。OnEvent 里只处理 CHARTEVENT_MOUSE_MOVE,且控件隐藏(m_is_visible 为假)时直接退出,再从 lparam / dparam 拆出 x、y 坐标继续做命中测试。 开 MT5 建个 CMenuItem 派生类,把 m_area_color_hover 设成迥异于默认色的数值,编译挂到图表上移动鼠标,能立刻看到焦点分支是否按类型生效。外汇与贵金属 GUI 脚本同样面临这类事件开销,高频鼠标移动下颜色重绘可能影响轻量终端响应,属低风险但需实测。
class="kw">public: class=class="str">"cmt">//--- Changing the class="type">class="kw">color of the control objects class="type">void ChangeObjectsColor(class="type">void); class=class="str">"cmt">//--- Reset class="type">class="kw">color class="type">void ResetColors(class="type">void); class=class="str">"cmt">//--- }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Changing the object class="type">class="kw">color when the cursor is hovering over it | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMenuItem::ChangeObjectsColor(class="type">void) { class=class="str">"cmt">//--- Leave, if this item has a context menu and it is enabled if(m_type_menu_item==MI_HAS_CONTEXT_MENU && m_context_menu_state) class="kw">return; class=class="str">"cmt">//--- Code block for simple items and items containing a context menu if(m_type_menu_item==MI_HAS_CONTEXT_MENU || m_type_menu_item==MI_SIMPLE) { class=class="str">"cmt">//--- If there is a focus if(CElement::MouseFocus()) { m_icon.State(m_item_state); m_area.BackColor((m_item_state)? m_area_color_hover : m_area_color_off); m_label.Color((m_item_state)? m_label_color_hover : m_label_color_off); if(m_item_state) m_arrow.State(true); } class=class="str">"cmt">//--- If there is no focus else { m_arrow.State(class="kw">false); m_area.BackColor(m_area_color); m_label.Color((m_item_state)? m_label_color : m_label_color_off); } } class=class="str">"cmt">//--- Block code for checkbox items and radio items else if(m_type_menu_item==MI_CHECKBOX || m_type_menu_item==MI_RADIOBUTTON) { m_icon.State(CElement::MouseFocus()); m_area.BackColor((CElement::MouseFocus())? m_area_color_hover : m_area_color); m_label.Color((CElement::MouseFocus())? m_label_color_hover : m_label_color); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Reset the item class="type">class="kw">color | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMenuItem::ResetColors(class="type">void) { CElement::MouseFocus(class="kw">false); m_area.BackColor(m_area_color); m_label.Color(m_label_color); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Event handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMenuItem::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) { if(id==CHARTEVENT_MOUSE_MOVE) { class=class="str">"cmt">//--- If the control is not hidden if(!CElement::m_is_visible) class="kw">return; class=class="str">"cmt">//--- Identify the focus class="type">int x=(class="type">int)lparam; class="type">int y=(class="type">int)dparam;
◍ 菜单项的鼠标命中与定时重绘
在自定义控件里,鼠标焦点的判定往往只靠四个边界比较。上面这段把 x、y 坐标夹在 X()~X2() 与 Y()~Y2() 之间,命中即调用 CElement::MouseFocus,随后直接 return 退出当前分发逻辑,避免后续误触发。 菜单项还挂了定时器事件 OnEventTimer,它不做复杂运算,只调 ChangeObjectsColor() 刷新控件颜色。对 MT5 界面开发来说,这种轻量定时重绘能把 hover、选中态的视觉延迟压到最低,你在 OnInit 里给控件 EnableTimer(100) 就能直观看到 100ms 级别的色彩跟随。 外汇与贵金属图表上的自定义面板属于高风险环境下的辅助工具,任何视觉反馈都只是提示,不构成方向判断。
CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2()); class="kw">return; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Timer | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMenuItem::OnEventTimer(class="type">void) { class=class="str">"cmt">//--- Changing the class="type">class="kw">color of the form objects ChangeObjectsColor(); }
测试附加一个菜单项
当您把EA交易载入到图表时, 您将看到一个带有菜单项的表单, 如下面屏幕截图所示. 您在这里看不到它, 因为默认的元件背景颜色和表单背景色是一样的. 您已经可以使用类的功能来修改对象的许多属性- 如有需要, 包括表单和菜单项元件. 图 5. 把菜单项元件添加到图表上的测试 元件已经添加到图表上了, 不过如果您尝试移动表单, 元件还是处在原来的位置, 并且如果您把鼠标光标掠过它, 它的外观也不变. 让我们创建一个方法来把元件指针添加到库中. 这样所有元件的属性就可以通过在一个循环中调用它们来管理了. 让我们把这个方法称为 CWndContainer::AddToElementsArray (). 它将有两个参数: (1) 元件将要附加的表单索引编号 (2) 将要保存到库中的元件对象指针. 在这个方法的开头, 需要检查库中是否含有一个或多个表单. 如果没有表单, 就会在日志中打印消息说明, 在把元件附加到表单之前, 表单必须首先加入库中. 然后还要检查是否超过了数组大小. 如果没有问题, 然后 (1)把指针加到附加表单的数组中 (2) 元件对象加到通用对象数组中 (3)保存元件的标识编号 (4)元件计数器加一. 这还不是此方法的最终版本, 我们晚点还会回来修改它. 当前版本的代码显示如下: 我们之前使用过的当前版本的 CWndContainer::AddToElementsArray () 方法已经足够用来测试EA交易了. 在 CProgram::CreateMenuItem () 方法的末尾, 在创建元件之后, 添加一行如下的代码. 如果修改过的文件编译过了, 并把EA载入到图表中, 那么当表单被移动时, 菜单项元件也会被一起移动, 并且当鼠标光标掠过时, 它的对象也会改变它们的外观了: 图 6. 菜单项元件作为图形界面部分的测试 [CODE] <span class="comment">//+------------------------------------------------------------------+</span>
| <span class="comment">// | Adds a pointer to the element array | </span> |
|---|
<span class="comment">//+------------------------------------------------------------------+</span> <span class="keyword">void</span> CWndContainer::AddToElementsArray(<span class="keyword">const</span> <span class="keyword">int</span> window_index,CElement &object) { <span class="comment">//--- If the base does not contain forms for controls</spa
「让表单收起时菜单项跟着隐身」
表单最小化后菜单项仍挂在图上,是因为图表事件统一由 CWndEvents 经基类 CWndContainer 派发,而它并不知道按钮被点了。MQL 里没有现成的最小化通知,只能靠 EventChartCustom() 自己造一个自定义事件塞进队列。 CWindow::OnEvent() 捕获 CHARTEVENT_OBJECT_CLICK 后,比对 sparam 与对象名一致就调 RollUp(),末尾用 EventChartCustom(m_chart_id, ON_WINDOW_ROLLUP, CElement::Id(), m_subwin, "") 发信号;Unroll() 同理用 ON_WINDOW_UNROLL。第三、四参数 lparam/dparam 是元件 id 和子窗口号,后续用来排掉别的程序或别的控件误发的事件。 自定义事件号写在 Defines.mqh:ON_WINDOW_UNROLL 为 1,ON_WINDOW_ROLLUP 为 2。CWndEvents 里声明 OnWindowRollUp/OnWindowUnroll,并在 ChartEventCustom() 中按宏名对应处理,循环里对每个元件调 CElement::Hide() 或 Show()。注意 CWindow::Show() 也得补实现,否则循环调用会断。 编译载入后实测:点最小化,菜单项随表单收起而隐藏;最大化恢复显示。外汇与贵金属图表挂 EA 做界面改造属高风险操作,建议先在策略测试器或模拟图表验证事件链路。
class=class="str">"cmt">//--- Events class="macro">#define ON_WINDOW_UNROLL(class="num">1) class=class="str">"cmt">// Form maximization class="macro">#define ON_WINDOW_ROLLUP(class="num">2) class=class="str">"cmt">// Form minimization class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Minimizes the window | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWindow::RollUp(class="type">void) { class=class="str">"cmt">//--- Change the button class=class="str">"cmt">//--- Set and store the size class=class="str">"cmt">//--- Disable the button class=class="str">"cmt">//--- Minimized state of the form class=class="str">"cmt">//--- If this is an indicator with a set height and with the sub-window minimization mode, class=class="str">"cmt">// set the size of the indicator sub-window class=class="str">"cmt">//--- Send a message about it ::EventChartCustom(m_chart_id,ON_WINDOW_ROLLUP,CElement::Id(),m_subwin,""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Maximizes the window | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWindow::Unroll(class="type">void) { class=class="str">"cmt">//--- Change the button class=class="str">"cmt">//--- Set and store the size class=class="str">"cmt">//--- Disable the button class=class="str">"cmt">//--- Maximized state of the form class=class="str">"cmt">//--- If this is an indicator with a set height and with the sub-window minimization mode, class=class="str">"cmt">// set the size of the indicator sub-window class=class="str">"cmt">//--- Send a message about it ::EventChartCustom(m_chart_id,ON_WINDOW_UNROLL,CElement::Id(),m_subwin,""); } class CWndEvents : class="kw">public CWndContainer { class="kw">private: class=class="str">"cmt">//--- Minimizing/maximizing the form class="type">bool OnWindowRollUp(class="type">void); class="type">bool OnWindowUnroll(class="type">void); class=class="str">"cmt">//--- }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ON_WINDOW_ROLLUP event | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CWndEvents::OnWindowRollUp(class="type">void) { class=class="str">"cmt">//--- If the signal is to minimize the form if(m_id!=CHARTEVENT_CUSTOM+ON_WINDOW_ROLLUP) class="kw">return(class="kw">false);
◍ 窗口收展时如何批量切换控件显隐
在自建 GUI 里做表单收起(roll up)和展开(unroll)时,核心不是去动每个图形对象,而是遍历容器里的元素指针统一下指令。下面两段事件处理分别对应收起与展开:收起时把除 CWindow 本身外的全部元素 Hide(),展开时再把非下拉类的元素 Show(),下拉控件保持原状态避免误触。
if(m_lparam==m_windows[class="num">0].Id() && (class="type">int)m_dparam==m_subwin) { class="type">int elements_total=CWndContainer::ElementsTotal(class="num">0); for(class="type">int e=class="num">0; e<elements_total; e++) { if(m_wnd[class="num">0].m_elements[e].ClassName()!="CWindow") m_wnd[class="num">0].m_elements[e].Hide(); } }
if(m_lparam==m_windows[class="num">0].Id() && (class="type">int)m_dparam==m_subwin) { class="type">int elements_total=CWndContainer::ElementsTotal(class="num">0); for(class="type">int e=class="num">0; e<elements_total; e++) { if(m_wnd[class="num">0].m_elements[e].ClassName()!="CWindow") m_wnd[class="num">0].m_elements[e].Hide(); } } class="type">bool CWndEvents::OnWindowUnroll(class="type">void) { if(m_id!=CHARTEVENT_CUSTOM+ON_WINDOW_UNROLL) class="kw">return(class="kw">false); if(m_lparam==m_windows[class="num">0].Id() && (class="type">int)m_dparam==m_subwin) { class="type">int elements_total=CWndContainer::ElementsTotal(class="num">0); for(class="type">int e=class="num">0; e<elements_total; e++) { if(m_wnd[class="num">0].m_elements[e].ClassName()!="CWindow") if(!m_wnd[class="num">0].m_elements[e].IsDropdown()) m_wnd[class="num">0].m_elements[e].Show(); } } class="kw">return(true); } class="type">void CWindow::Show(class="type">void) { for(class="type">int i=class="num">0; i<ObjectsElementTotal(); i++) Object(i).Timeframes(OBJ_ALL_PERIODS); CElement::m_is_visible=true; CElement::MouseFocus(class="kw">false); m_button_close.MouseFocus(class="kw">false); m_button_close.State(class="kw">false); }
窗口容器空置时直接退出事件分发
在 MT5 自定义界面库里,CWndEvents::ChartEvent 是统一接收图表事件的总入口。它在真正处理任何鼠标、自定义或属性变更之前,先做一次容器判空。 代码里用 CWndContainer::WindowsTotal()<1 做守卫:若当前没有任何已注册的窗口对象,函数直接 return,避免对空容器做无意义遍历。这一行能省掉不少无效计算,尤其在指标刚加载、界面还没构造成功的几帧内。 随后 InitChartEventsParams 把 id/lparam/dparam/sparam 写进内部字段,再依次跑 ChartEventCustom、CheckElementsEvents、ChartEventMouseMove、ChartEventChartChange。想验证的话,在 OnInit 里故意不创建任何 CWnd 派生窗口,用 Print 打点能看到 ChartEvent 每次都被守卫语句截胡。 外汇与贵金属图表上跑这类 GUI 逻辑属高风险环境,事件频繁触发时空判断空能降低卡顿概率,但是否生效仍取决于具体品种波动与终端负载。
if(OnWindowUnroll()) class="kw">return; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handling program events | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWndEvents::ChartEvent(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">//--- Leave, if the array is empty if(CWndContainer::WindowsTotal()<class="num">1) class="kw">return; class=class="str">"cmt">//--- Initialization of the event parameter fields InitChartEventsParams(id,lparam,dparam,sparam); class=class="str">"cmt">//--- Custom event ChartEventCustom(); class=class="str">"cmt">//--- Checking events of the interface elements CheckElementsEvents(); class=class="str">"cmt">//--- Mouse movement event ChartEventMouseMove(); class=class="str">"cmt">//--- The chart properties change event ChartEventChartChange(); }
「把工具请下神坛」
这一节把前面六篇做的菜单项控件、CWindow 表单增强和 CWndEvents 事件类收了个口。当前阶段库文件已经能跑:MQL4 包 15.02 KB、MQL5 包 19.1 KB,直接丢进对应终端就能测界面响应。 作者在原讨论里确认过,这套库能为自定义测试器搭图形面板,2D 图表类也已存在(改自标准库),但完整绘图库要等约 20 篇系列更完才稳。别把它当黑箱圣杯——高频重报价环境下界面延迟和真实成交偏差,只能靠你自己开 MT5 用历史+实时对照去嗅。 下一篇原计划做分隔线与上下文菜单类,想验证就先下 zip 把菜单项跑通,再决定跟不跟后续封装。
Список статей (глав) второй части: Графические интерфейсы II: Элемент <span class="class="type">class="kw">string">"菜单项"。</span> (Глава <span class="number">class="num">1</span>) Графические интерфейсы II: Элементы <span class="class="type">class="kw">string">"分界线"。</span> и <span class="class="type">class="kw">string">"上下文菜单"。</span> (Глава <span class="number">class="num">2</span>) Графические интерфейсы II: Настройка обработчиков событий библиотеки (Глава <span class="number">class="num">3</span>) Графические интерфейсы II: Элемент <span class="class="type">class="kw">string">"主菜单"</span> (Глава <span class="number">class="num">4</span>)