基于 .Net 框架和 C# 开发图形界面(第二部分):另外的图形元素·进阶篇
🧩

基于 .Net 框架和 C# 开发图形界面(第二部分):另外的图形元素·进阶篇

(2/3)· 从选项卡到数字选择器,把 WinForms 交互面板的剩余拼图一次补齐

进阶 第 2/3 篇
很多交易者把 EA 面板停留在按钮加文本框,遇到多策略切换就只能堆窗口。其实 .Net 的 TabControl、CheckBox 这类控件早就能在 MT5 里直接用,只是多数人没摸到 GuiController 的扩展入口。

「TabControl 的选项卡切换事件怎么抓」

在 MT5 的 GuiController 表单里,TabControl 是把同类控件按面板分组的实用元件。它只暴露一个 TabIndexChange 事件,用来通知程序用户切到了哪个选项卡,不需要自己写轮询去判断当前页。 这个事件会带两个关键参数:lparam 是用户所选选项卡的索引(从 0 起算),sparam 是所选选项卡的名称。比如切到第一个页,控制台会打出「选择了新页面. Index: 0 Name: tabPage1」这样的 trace,直接能看到索引和名字的对应关系。 WindowsForm 要求同表单内所有元素名称唯一,所以不同选项卡里同类型控件也得单独命名;多数时候你只关心具体控件被按了没,不太管它在哪一页。但遇到要按页区分逻辑的场景,GuiController 给的这套事件接口就够用了。 下面这段是从测试 EA 里抽的轮询事件队列代码,专门把 TabIndexChange 捞出来打印。跑起来后手动点选项卡,就能在专家日志里验证参数格式。

MQL5 / C++
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 里单独写分支,否则部分选中时子项可能既不解锁也不锁定,界面会卡在半残状态。外汇与贵金属波动剧烈,这类开关只管风控项可见性,不替代仓位计算。

MQL5 / C++
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 状态误判可能让下单逻辑在高波动时跑偏,务必在策略测试器里复验。

MQL5 / C++
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 并打印参数。

MQL5 / C++
<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 &lt; GuiController::EventsTotal(); i++)
{
&nbsp;&nbsp;<span class="keyword">class="type">int</span> id;
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">string</span> el_name;
&nbsp;&nbsp;<span class="keyword">class="type">long</span> lparam;
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> dparam;
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">string</span> sparam;
&nbsp;&nbsp;GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
&nbsp;&nbsp;<span class="keyword">if</span>(id == ComboBoxChange)
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp; <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>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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>);
&nbsp;&nbsp;&nbsp;&nbsp; <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);
&nbsp;&nbsp;}
}

数字框怎么跟着品种变格式

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 里每个品种单独格式的来源。 外汇和贵金属点差跳变快,这种动态绑定若步长设错,面板填单可能卡在非法价格附近,实盘前务必在策略测试器里切几个品种验一遍。

MQL5 / C++
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 里接这套,调参界面能实时限价区间;这类品种杠杆高、滑点跳变频繁,边界设错可能让手动改参直接爆仓,上线前务必在策略测试器里手点一遍控件验证。

MQL5 / C++
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);
}
让小布替你跑这套
这些控件诊断与面板模板小布盯盘已内置,打开对应品种页即可看到现成结构,你只需改事件逻辑。

常见问题

通过 GuiController 向 C# 侧发送添加选项卡指令,每个页面对应独立容器,事件回传时带页索引区分来源。
可以,控件状态改变会触发 C# 事件并经由交互层回传 MQL,概率上延迟取决于管道调用频率。
小布盯盘内置了常见控件结构示例,可导出基础代码框架,省去从零搭 GuiController 的重复劳动。
前者强制步进与上下限,避免脏输入;后者需自行校验,适合自由格式。外汇贵金属操作属高风险,数值误设可能引发行情误判。
增删项需走 AddItem 接口并刷新句柄,否则旧引用可能指向失效元素导致异常。