基于 .Net 框架和 C# 开发图形界面(第二部分):另外的图形元素·进阶篇
(2/3)· 从选项卡到数字选择器,把 WinForms 交互面板的剩余拼图一次补齐
「TabControl 的选项卡切换事件怎么抓」
在 MT5 的 GuiController 表单里,TabControl 是把同类控件按面板分组的实用元件。它只暴露一个 TabIndexChange 事件,用来通知程序用户切到了哪个选项卡,不需要自己写轮询去判断当前页。 这个事件会带两个关键参数:lparam 是用户所选选项卡的索引(从 0 起算),sparam 是所选选项卡的名称。比如切到第一个页,控制台会打出「选择了新页面. Index: 0 Name: tabPage1」这样的 trace,直接能看到索引和名字的对应关系。 WindowsForm 要求同表单内所有元素名称唯一,所以不同选项卡里同类型控件也得单独命名;多数时候你只关心具体控件被按了没,不太管它在哪一页。但遇到要按页区分逻辑的场景,GuiController 给的这套事件接口就够用了。 下面这段是从测试 EA 里抽的轮询事件队列代码,专门把 TabIndexChange 捞出来打印。跑起来后手动点选项卡,就能在专家日志里验证参数格式。
for(class="kw">static class="type">int i = class="num">0; i < GuiController::EventsTotal(); i++) { class="type">int id; class="type">class="kw">string el_name; class="type">long lparam; class="type">class="kw">double dparam; class="type">class="kw">string sparam; GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam); if(id == TabIndexChange) printf("选择了新页面. Index: " + (class="type">class="kw">string)lparam + " Name: " + sparam); }
用复选框状态联动子开关
复选框在 MT5 面板里不止是打勾,它有三态:未选中对应 0、选中对应 1、部分选中对应 2。GuiController 会在用户点击后通过 CheckBoxChange 事件把状态值塞进 lparam 传给 EA,EA 据此判断该解锁还是锁死下游控件。 演示里 EA 盯着「启用 EURUSD 交易」和「启用 GBPUSD 交易」两个主复选框。只要其中一个被勾上,就立刻给它的「允许获利」「允许止损」子复选框发 ElementEnable 事件置为 true;取消勾选则发 false 锁住。控制逻辑全在 EA 侧,表单只负责按事件改可访问性。 下面这段是 EA 处理复选框联动的核心。OnTimer 里轮询 GuiController 的事件队列,碰到 id == CheckBoxChange 就转交处理函数;ChangeEnableStopAndProfit 按 el_name 把同一个 lparam 广播给对应的 Profit / Stop 子项。 别把状态值当摆设 lparam 的 0/1/2 直接决定子控件死活,若你面板里加了 Indeterminate 态,记得在 EA 里单独写分支,否则部分选中时子项可能既不解锁也不锁定,界面会卡在半残状态。外汇与贵金属波动剧烈,这类开关只管风控项可见性,不替代仓位计算。
class="kw">namespace System.Windows.Forms { class=class="str">"cmt">// class=class="str">"cmt">// 概述: class=class="str">"cmt">// 指定控件的状态,如复选框,可以选中、取消选中, class=class="str">"cmt">// 或设置为不确定状态。 class="kw">public enum CheckState { class=class="str">"cmt">// class=class="str">"cmt">// 概述: class=class="str">"cmt">// 控件没有被选中 Unchecked = class="num">0, class=class="str">"cmt">// class=class="str">"cmt">// 概述: class=class="str">"cmt">// 控件被选中 Checked = class="num">1, class=class="str">"cmt">// class=class="str">"cmt">// 概述: class=class="str">"cmt">// 控件不确定状态,不确定状态的控件通常有阴影 class=class="str">"cmt">// 外观 Indeterminate = class="num">2 } } class="type">void OnTimer() { class=class="str">"cmt">//-- 根据计时器取得新的事件 for(class="kw">static class="type">int i = class="num">0; i < GuiController::EventsTotal(); i++) { class="type">int id; class="type">class="kw">string el_name; class="type">long lparam; class="type">class="kw">double dparam; class="type">class="kw">string sparam; GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam); if(id == CheckBoxChange) ChangeEnableStopAndProfit(el_name, id, lparam, dparam, sparam); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 修改启用止损和获利 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void ChangeEnableStopAndProfit(class="type">class="kw">string el_name, class="type">int id, class="type">long lparam, class="type">class="kw">double dparam, class="type">class="kw">string sparam) { class="type">int id_enable = ElementEnable; if(el_name == "EURUSDEnable") { GuiController::SendEvent("EURUSDProfit", id_enable, lparam, dparam, sparam); GuiController::SendEvent("EURUSDStop", id_enable, lparam, dparam, sparam); } else if(el_name == "GBPUSDEnable") { GuiController::SendEvent("GBPUSDProfit", id_enable, lparam, dparam, sparam); GuiController::SendEvent("GBPUSDStop", id_enable, lparam, dparam, sparam); } }
◍ RadioButtonChange 事件里的真假切换
MT5 自定义面板里的单选按钮,本质是让用户在一组互斥选项里挑一个。但它往 EA 抛事件的方式有点绕:每次切换,EA 会收到两次 RadioButtonChange,一次来自刚被取消的那个,一次来自新选中的那个,两个事件共用同一个标识符。 lparam 就是区分动作的关键——值为 true 表示该项被选中,false 表示被取消。下面这段循环把控件事件队列扫一遍,按 id 过滤出单选按钮变动,再靠 lparam 分流打印。 实际跑起来,终端日志会先刷出「您取消选择了…」再刷「您选择了…」,顺序和视觉点击一致。做多周期切换或策略模式切换时,记得在 lparam==true 的分支里才真正改 EA 内部状态,否则会被取消事件误触发。外汇与贵金属波动剧烈,这类 UI 状态误判可能让下单逻辑在高波动时跑偏,务必在策略测试器里复验。
for(class="kw">static class="type">int i = class="num">0; i < GuiController::EventsTotal(); i++) { class="type">int id; class="type">class="kw">string el_name; class="type">long lparam; class="type">class="kw">double dparam; class="type">class="kw">string sparam; GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam); else if(id == RadioButtonChange) { if(lparam == true) printf("您选择了 " + sparam); else printf("您取消选择了 " + sparam); } }
「组合框的两种模式与联动解锁」
组合框在 GUI 里主要跑两种模式:一种是允许用户输入列表之外的新值,另一种只给预设菜单、禁止自定义。第三种隐藏菜单项的模式极少用,实际开发基本忽略。模式切换靠 DropDownStyle 属性一次性写死,guiController 不提供运行时改类型的事件。 控制器能跟踪的是选择动作和输入新值,所以 ComboBox 只挂两个事件:自身的 ComboBoxChange 和 TextChange。演示表单里放了俩组合框,comboBox1 选 MT4/MT5 平台,comboBox2 选交易品种,后者初始被锁。 用户一旦在 comboBox1 里选定平台,代码立刻给 comboBox2 发 ElementEnable 解锁,之后才能挑品种。终端会逐行打印类似「在 GBPUSD 上组合框 'comboBox2' 已经改变」的日志,验证联动是否生效。外汇与贵金属品种切换涉及点差和滑点波动,实盘前请在策略测试器里先跑通事件链。 下面这段是联动核心:遍历事件队列,命中 ComboBoxChange 且来源是 comboBox1 时,直接解锁 comboBox2 并打印参数。
<span class="keyword">for</span>(<span class="keyword">class="kw">static</span> <span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i < GuiController::EventsTotal(); i++) { <span class="keyword">class="type">int</span> id; <span class="keyword">class="type">class="kw">string</span> el_name; <span class="keyword">class="type">long</span> lparam; <span class="keyword">class="type">class="kw">double</span> dparam; <span class="keyword">class="type">class="kw">string</span> sparam; GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam); <span class="keyword">if</span>(id == ComboBoxChange) { <span class="keyword">if</span>(el_name == <span class="class="type">class="kw">string">"comboBox1"</span>) <span style="class="type">color:rgb(class="num">153, class="num">153, class="num">153);"> <span style="class="type">color:rgb(class="num">102, class="num">102, class="num">102);">class=class="str">"cmt">//-- 用户选择平台后立即取消阻止交易品种列表:</span></span> GuiController::SendEvent(<span class="class="type">class="kw">string">"comboBox2"</span>, ElementEnable, <span class="number">class="num">1</span>, <span class="number">class="num">0.0</span>, <span class="class="type">class="kw">string">""</span>); <span class="functions">printf</span>(<span class="class="type">class="kw">string">"组合框 &class="macro">#x27;"</span> + el_name + <span class="class="type">class="kw">string">"&class="macro">#x27; 已改变 "</span> + sparam); } }
数字框怎么跟着品种变格式
NumericUpDown 是 GUI 控制器里第一个被塞进交易面板类系统的控件,它只收数字、带步进小箭头,还能按小数位显示。它向外抛四种事件:NumericChange 传新值,NumericFormatChange 用 lparam 带位数、dparam 带步长,NumericMaxChange / NumericMinChange 分别报上下限。 用户侧唯一能触发的就是 NumericChange,EA 收到数值变动通知后自行处理。但小数位、步长、最大最小值没法在表单里写死,必须按 EA 逻辑和品种数据类型在启动阶段动态设。 测试 EA 在 OnInit 里按当前品种抓 ask、bid、价格步长和 Digits(),再调 NumericSet 填进表单。跑在 XAUUSD 上可能看到 2 位小数加 0.01 步长,跑在 EURUSD 上则是 5 位小数加 0.00001 步长——这就是图 15 里每个品种单独格式的来源。 外汇和贵金属点差跳变快,这种动态绑定若步长设错,面板填单可能卡在非法价格附近,实盘前务必在策略测试器里切几个品种验一遍。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 交易初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { if(ElementType != WINFORM_HIDE) EventSetMillisecondTimer(class="num">100); else EventSetMillisecondTimer(class="num">1000); class="kw">switch(ElementType) { ... case WINFORM_NUMERIC: { GuiController::ShowForm(assembly, "NumericForm"); class="type">class="kw">double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK); class="type">class="kw">double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID); class="type">class="kw">double price_step = NormalizeDouble(SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_SIZE), Digits()); class="type">long digits = (class="type">long)Digits(); GuiController::SendEvent("NumericForm", TextChange, class="num">0, class="num">0.0, "NumericForm(" + Symbol() + ")"); NumericSet("StopLoss", Digits(), ask, (class="type">class="kw">double)LONG_MAX, class="num">0.0, price_step); NumericSet("TakeProfit", Digits(), bid, (class="type">class="kw">double)LONG_MAX, class="num">0.0, price_step); class="kw">break; } ... } class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置 NumericUpDown 参数 | class=class="str">"cmt">//| name - NumericUpDown 元素的名称 | class=class="str">"cmt">//| digits - 交易品种的小数位数 | class=class="str">"cmt">//| init - 初始化双精度值 | class=class="str">"cmt">//| max - 最大值 | class=class="str">"cmt">//| min - 最小值 |
◍ 用 SendEvent 把数值控件推到 GUI
上面这段函数把外部算好的数值边界和步长,一次性写进 MT5 自定义面板的数字框。五个入参分别是控件名、小数位、初值、上限、下限、步长,调用方算好直接丢进来即可。 内部先抓四个事件 ID:格式变更、值变更、最大值、最小值,再各发一次 SendEvent。注意顺序——先发 max 和 min 设边界,再发 init 定初值,最后发格式变更把 digits 和 step 绑上去,乱序可能导致面板先渲染了越界值。 在外汇或贵金属 EA 里接这套,调参界面能实时限价区间;这类品种杠杆高、滑点跳变频繁,边界设错可能让手动改参直接爆仓,上线前务必在策略测试器里手点一遍控件验证。
class=class="str">"cmt">//| step - 变化步长 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void NumericSet(class="type">class="kw">string name, class="type">long digits, class="type">class="kw">double init, class="type">class="kw">double max, class="type">class="kw">double min, class="type">class="kw">double step) { class="type">int id_foramt_change = NumericFormatChange; class="type">int id_change = NumericChange; class="type">int id_max = NumericMaxChange; class="type">int id_min = NumericMinChange; class="type">long lparam = class="num">0; class="type">class="kw">double dparam = class="num">0.0; class="type">class="kw">string sparam = ""; GuiController::SendEvent(name, id_max, lparam, max, sparam); GuiController::SendEvent(name, id_min, lparam, min, sparam); GuiController::SendEvent(name, id_change, lparam, init, sparam); GuiController::SendEvent(name, id_foramt_change, digits, step, sparam); }