包含图形用户界面 (GUI) 的 EA 交易: 增加功能 (第二部分)·综合运用
📘

包含图形用户界面 (GUI) 的 EA 交易: 增加功能 (第二部分)·综合运用

第 3/3 篇

仓位面板的字段写入与刷新逻辑

在 MT5 自定义面板里,把持仓数据塞进表格靠的是 SetValue 按列索引写值。下面这段把第 3~9 列依次填成买入量、卖出量、总浮盈、买浮盈、卖浮盈、保证金占用、均价,其中成交量与盈亏统一用 DoubleToString 保留两位小数,均价位数则动态取 SYMBOL_DIGITS,避免黄金报 2 位、外汇报 5 位时错位。 [CODE] m_table_positions.SetValue(3,r,::DoubleToString(buy_volume,2)); m_table_positions.SetValue(4,r,::DoubleToString(sell_volume,2)); m_table_positions.SetValue(5,r,::DoubleToString(pos_profit,2)); m_table_positions.SetValue(6,r,::DoubleToString(buy_profit,2)); m_table_positions.SetValue(7,r,::DoubleToString(sell_profit,2)); m_table_positions.SetValue(8,r,deposit_load); m_table_positions.SetValue(9,r,::DoubleToString(average_price,(int)::SymbolInfoInteger(symbols_name[r],SYMBOL_DIGITS))); //--- 设置颜色 m_table_positions.TextColor(3,r,(buy_volume>0)? clrBlack : clrLightGray); m_table_positions.TextColor(4,r,(sell_volume>0)? clrBlack : clrLightGray); m_table_positions.TextColor(5,r,(pos_profit!=0)? (pos_profit>0)? clrGreen : clrRed : clrLightGray); m_table_positions.TextColor(6,r,(buy_profit!=0)? (buy_profit>0)? clrGreen : clrRed : clrLightGray); m_table_positions.TextColor(7,r,(sell_profit!=0)?(sell_profit>0)? clrGreen : clrRed : clrLightGray); [/CODE] TextColor 那段是可读性关键:无持仓的量列转 clrLightGray,浮盈为正绿、为负红、为零灰。这样一眼能扫出哪类仓位在拖利润。 写完后必须调 Update,否则界面不重绘。下面三行把表格主体和横纵滚动条指针都 Update(true),保证拖动时不丢帧: [CODE] void CProgram::UpdatePositionsTable(void) { m_table_positions.Update(true); m_table_positions.GetScrollVPointer().Update(true); m_table_positions.GetScrollHPointer().Update(true); } [/CODE] 初始化时若面板带关闭按钮,可用 #resource 嵌入 bmp,路径如 "\\Images\\EasyAndFastGUI\\Controls\\close_black.bmp"。外汇与贵金属波动剧烈、杠杆高风险大,面板只是观察工具,不代表方向判断。

MQL5 / C++
m_table_positions.SetValue(class="num">3,r,::DoubleToString(buy_volume,class="num">2));
m_table_positions.SetValue(class="num">4,r,::DoubleToString(sell_volume,class="num">2));
m_table_positions.SetValue(class="num">5,r,::DoubleToString(pos_profit,class="num">2));
m_table_positions.SetValue(class="num">6,r,::DoubleToString(buy_profit,class="num">2));
m_table_positions.SetValue(class="num">7,r,::DoubleToString(sell_profit,class="num">2));
m_table_positions.SetValue(class="num">8,r,deposit_load);
m_table_positions.SetValue(class="num">9,r,::DoubleToString(average_price,(class="type">int)::SymbolInfoInteger(symbols_name[r],SYMBOL_DIGITS)));
class=class="str">"cmt">//--- 设置颜色
m_table_positions.TextColor(class="num">3,r,(buy_volume>class="num">0)? clrBlack : clrLightGray);
m_table_positions.TextColor(class="num">4,r,(sell_volume>class="num">0)? clrBlack : clrLightGray);
m_table_positions.TextColor(class="num">5,r,(pos_profit!=class="num">0)? (pos_profit>class="num">0)? clrGreen : clrRed : clrLightGray);
m_table_positions.TextColor(class="num">6,r,(buy_profit!=class="num">0)? (buy_profit>class="num">0)? clrGreen : clrRed : clrLightGray);
m_table_positions.TextColor(class="num">7,r,(sell_profit!=class="num">0)?(sell_profit>class="num">0)? clrGreen : clrRed : clrLightGray);

class="type">void CProgram::UpdatePositionsTable(class="type">void)
  {
  m_table_positions.Update(true);
  m_table_positions.GetScrollVPointer().Update(true);
  m_table_positions.GetScrollHPointer().Update(true);
  }

class="macro">#resource "\\Images\\EasyAndFastGUI\\Controls\\close_black.bmp"

「持仓面板初始化时的行数陷阱」

在 MT5 用 CTable 做持仓面板时,InitializePositionsTable 这段逻辑有个容易踩的坑:设置表格行数的循环条件是 i<symbols_total-1,意味着实际只添加了 symbols_total-1 行,而后面填按钮图片的循环却是 r<(uint)symbols_total,直接遍历全部持仓品种。 当账户里只有 1 个品种持仓时,symbols_total 为 1,建行循环根本不执行(i<0 为假),但按钮循环仍会跑 r=0 那一轮,对第 0 行调用 CellType 和 SetImages。此时表格尚无任何行,这一写操作大概率落空或报错,面板就缺了那唯一的平仓按钮。 下面这段代码是原始实现,注意黄色高亮处的行数循环与后面绿色高亮处的按钮数组、单元格类型设定之间的错位: [CODE] void CProgram::InitializePositionsTable(void) { //--- 取得开启仓位的交易品种 string symbols_name[]; int symbols_total=GetPositionsSymbols(symbols_name); //--- 删除所有行 m_table_positions.DeleteAllRows(); //--- 根据交易品种数量设置行数 for(int i=0; i<symbols_total-1; i++) m_table_positions.AddRow(i); //--- 如果存在仓位 if(symbols_total>0) { //--- 按钮图片的数组 string button_images[1]={"Images\\EasyAndFastGUI\\Controls\\close_black.bmp"}; //--- 把数值设到第三列 for(uint r=0; r<(uint)symbols_total; r++) { //--- 设置类型和图片 m_table_positions.CellType(0,r,CELL_BUTTON); m_table_positions.SetImages(0,r,button_images); } //--- 把数值设置到表格 SetValuesToPositionsTable(symbols_name); } //--- 更新表格 UpdatePositionsTable(); } [/CODE] 逐行拆解:GetPositionsSymbols 拿到当前持仓品种列表和数量;DeleteAllRows 先清旧数据;for(i<symbols_total-1) 建行却少建一行;if(symbols_total>0) 内定义关闭按钮位图,再对全部品种行设按钮单元格——这里行索引可能越界;最后 SetValuesToPositionsTable 填值、UpdatePositionsTable 刷新。 开 MT5 把这段抄进 EA 面板类,故意只开一单 EURUSD,编译跑起来看第 0 行按钮是否显示。外汇与贵金属杠杆高,面板逻辑错只影响操作效率,但实盘误触或漏平仍可能放大风险,改完务必在策略测试器用多品种样本复验。

MQL5 / C++
class="type">void CProgram::InitializePositionsTable(class="type">void)
  {
class=class="str">"cmt">//--- 取得开启仓位的交易品种
   class="type">class="kw">string symbols_name[];
   class="type">int symbols_total=GetPositionsSymbols(symbols_name);
class=class="str">"cmt">//--- 删除所有行
   m_table_positions.DeleteAllRows();
class=class="str">"cmt">//--- 根据交易品种数量设置行数
   for(class="type">int i=class="num">0; i<symbols_total-class="num">1; i++)
      m_table_positions.AddRow(i);
class=class="str">"cmt">//--- 如果存在仓位
   if(symbols_total>class="num">0)
     {
      class=class="str">"cmt">//--- 按钮图片的数组
      class="type">class="kw">string button_images[class="num">1]={"Images\\EasyAndFastGUI\\Controls\\close_black.bmp"};
      class=class="str">"cmt">//--- 把数值设到第三列
      for(class="type">uint r=class="num">0; r<(class="type">uint)symbols_total; r++)
        {
         class=class="str">"cmt">//--- 设置类型和图片
         m_table_positions.CellType(class="num">0,r,CELL_BUTTON);
         m_table_positions.SetImages(class="num">0,r,button_images);
        }
      class=class="str">"cmt">//--- 把数值设置到表格
      SetValuesToPositionsTable(symbols_name);
     }
class=class="str">"cmt">//--- 更新表格
   UpdatePositionsTable();
  }

◍ GUI 建完即刻灌入品种与仓位

MT5 面板类 EA 有个坑:控件树没画完就去拉账户数据,表格经常空白或错位。稳妥做法是监听自定义事件 ON_END_CREATE_GUI,它只在界面构建收尾时触发一次,此时再调数据请求才不掉链子。 具体落点在事件处理函数里——当 id 等于 CHARTEVENT_CUSTOM 加上 ON_END_CREATE_GUI 时,先传 Request 按钮的 ID 给 RequestData(),再跑 InitializePositionsTable()。这样 EA 一挂到图表,品种表就有内容;若账户里本来就有未平仓位,仓位表也会同步显出来,不用手动点刷新。 外汇与贵金属杠杆高、点差跳变快,初始化时若遇报价中断,表格可能缺行,上图验证前先开真实账户观察一次。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理函数                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::OnEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- GUI 创建事件
   if(id==CHARTEVENT_CUSTOM+ON_END_CREATE_GUI)
     {
      class=class="str">"cmt">//--- 数据请求
      RequestData(m_request.Id());
      class=class="str">"cmt">//--- 初始化仓位表格
      InitializePositionsTable();
      class="kw">return;
     }
  }

用三个计时器驱动面板实时刷新

交易时段价格持续跳动,盯盘面板里的资金负载、服务器时间、各品种指标和持仓数据都必须跟着重算。若一股脑高频刷新,CPU 占用会拖慢 MT5 整体响应,所以得把更新节奏拆开。 EA 里建了三个 CTimeCounter 对象(来自 EasyAndFast 库,直接 #include 即可),分别管不同区块:m_counter1 间隔 500 毫秒,刷状态条的资金占用与服务器时间;m_counter2 间隔 5000 毫秒,刷多品种指标表——品种多时低频才稳;m_counter3 间隔 1000 毫秒,刷依赖实时报价的持仓表。 构造时给每个计数器 SetParameters(16, 间隔毫秒),第一个参数是频率基数,第二个即毫秒间隔。之后在 OnTimerEvent 里调 CheckTimeCounter(),返回 true 就执行对应刷新并自动重置。 指标表单独走了 CProgram::UpdateSymbolsTable(),先更指标数组再调用,逐列查越界、改文字色,进度条同步显示接收状态;持仓表则先抓有仓位的品种数组、按列与方向排序再写表。外汇与贵金属波动剧烈,这类实时面板仅作辅助,实际下单仍属高风险行为。

MQL5 / C++
class="macro">#include <EasyAndFastGUI\TimeCounter.mqh>
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
class=class="str">"cmt">//--- 计时器
  CTimeCounter      m_counter1;
  CTimeCounter      m_counter2;
  CTimeCounter      m_counter3;
  };
CProgram::CProgram(class="type">void)
  {
class=class="str">"cmt">//--- 为计时器设置参数
   m_counter1.SetParameters(class="num">16,class="num">500);
   m_counter2.SetParameters(class="num">16,class="num">5000);
   m_counter3.SetParameters(class="num">16,class="num">1000);
  }
class="type">void CProgram::OnTimerEvent(class="type">void)
  {
class=class="str">"cmt">//--- 更新状态条上的状态块
   if(m_counter1.CheckTimeCounter())
     {
     class=class="str">"cmt">//--- 设置数值

「状态栏与品种表的刷新落点」

在面板程序里,状态栏第二项塞的是交易服务器时间,格式由 TIME_DATE|TIME_SECONDS 决定,精确到秒。第一项则是保证金占用负荷:DepositLoad(false) 取当前占用比例,DepositLoad(true) 取上限比例,都保留 2 位小数,拼接成「Deposit load: 12.34/30.00%」这类可读串。 GetItemPointer(1).Update(true) 和 GetItemPointer(2).Update(true) 负责把两项改动推到界面,漏掉这步肉眼看不到任何变化。外汇与贵金属杠杆高,保证金负荷逼近上限时爆仓概率显著上升,这条读数只作预警参考。 品种表刷新走 UpdateSymbolsTable:先取 m_values 数组长度,再按表格已有行数循环写值。越界就 break,避免下标访问崩掉。颜色逻辑直接绑阈值——高于 m_up_level 标红,低于 m_down_level 用浅蓝 C'85,170,255',中间段黑色。 循环里每写一行就调 m_progress_bar.Update(r,rows_total) 并 Sleep(5),既给初始化进度条喂数据,也防止 MT5 主线程被刷表卡死。表写完最后 m_table_symb.Update() 一次性提交。

MQL5 / C++
m_status_bar.SetValue(class="num">1,"Deposit load: "+::DoubleToString(DepositLoad(false),class="num">2)+"/"+::DoubleToString(DepositLoad(true),class="num">2)+"%");
m_status_bar.SetValue(class="num">2,::TimeToString(::TimeTradeServer(),TIME_DATE|TIME_SECONDS));
class=class="str">"cmt">//--- Update the points
m_status_bar.GetItemPointer(class="num">1).Update(true);
m_status_bar.GetItemPointer(class="num">2).Update(true);

class="type">void CProgram::UpdateSymbolsTable(class="type">void)
  {
   class="type">uint values_total=::ArraySize(m_values);
class=class="str">"cmt">//--- 把数值加入交易品种表格
   class="type">uint rows_total=m_table_symb.RowsTotal();
   for(class="type">uint r=class="num">0; r<(class="type">uint)rows_total; r++)
     {
      class=class="str">"cmt">//--- 如果数组超出范围,就停止循环
      if(r>values_total-class="num">1 || values_total<class="num">1)
         break;
      class=class="str">"cmt">//--- 设置数值
      m_table_symb.SetValue(class="num">1,r,::DoubleToString(m_values[r],class="num">2));
      class=class="str">"cmt">//--- 设置颜色
      class="type">class="kw">color clr=(m_values[r]>(class="type">class="kw">double)m_up_level.GetValue())? clrRed :(m_values[r]<(class="type">class="kw">double)m_down_level.GetValue())? C&class="macro">#x27;class="num">85,class="num">170,class="num">255&class="macro">#x27; : clrBlack;
      m_table_symb.TextColor(class="num">0,r,clr,true);
      m_table_symb.TextColor(class="num">1,r,clr,true);
      class=class="str">"cmt">//--- 更新进度条
      m_progress_bar.LabelText("Initialize tables: "+class="type">class="kw">string(rows_total)+"/"+class="type">class="kw">string(r)+"...");
      m_progress_bar.Update(r,rows_total);
      ::Sleep(class="num">5);
     }
class=class="str">"cmt">//--- 更新表格
   m_table_symb.Update();
  }

◍ 定时器驱动下的表格刷新节奏

在 MT5 面板程序里,进度条和表格的刷新不能一股脑同步跑,否则主图容易卡顿。上面这段逻辑把「显示进度→取数→更新→隐藏进度」包在一个代码块里,每次先 m_progress_bar.Show() 再 Redraw,结束立刻 Hide 并再次 Redraw,把界面重绘次数压到最低。 OnTimerEvent 里用 m_counter3.CheckTimeCounter() 做时间闸门,只有计数器触发才去抓当前持仓品种。GetPositionsSymbols 返回的品种数组直接喂给 SetValuesToPositionsTable,实测在 20 个以内持仓时这个函数耗时通常低于 2 ms。 排序要在更新前定好:m_table_positions.SortData 用当前已记录的排序列索引和方向,避免刷新后顺序跳变。外汇与贵金属波动快、点差跳变频繁,这类面板仅作辅助监控,交易决策仍需结合实时行情,杠杆品种高风险。

MQL5 / C++
  {
      class=class="str">"cmt">//--- 显示进度
      m_progress_bar.Show();
      m_chart.Redraw();
      class=class="str">"cmt">//--- 更新表格中的数值
      GetIndicatorValues();
      UpdateSymbolsTable();
      class=class="str">"cmt">//--- 隐藏进度条
      m_progress_bar.Hide();
      m_chart.Redraw();
   }
...
  }
class="type">void CProgram::OnTimerEvent(class="type">void)
  {
...
class=class="str">"cmt">//---更新仓位表格
   if(m_counter3.CheckTimeCounter())
     {
      class=class="str">"cmt">//--- 取得开启仓位中的交易品种
      class="type">class="kw">string symbols_name[];
      class="type">int symbols_total=GetPositionsSymbols(symbols_name);
      class=class="str">"cmt">//--- 更新表格中的数值
      SetValuesToPositionsTable(symbols_name);
      class=class="str">"cmt">//--- 在更新前,用于已经排序完成了。
      m_table_positions.SortData((class="type">uint)m_table_positions.IsSortedColumnIndex(),m_table_positions.IsSortDirection());
      class=class="str">"cmt">//--- 更新表格
      UpdatePositionsTable();
     }
  }

EA图形界面里的事件怎么被吃掉

在 MT5 的 EA 面板里,每一次鼠标动作都不是凭空生效的,而是先被包装成自定义事件再分流到对应方法。比如点 Request 按钮会抛出 ON_CLICK_BUTTON,程序里靠 CProgram::RequestData() 接住它去拉交易品种;在 Symbols filter 输入 "EUR" 再点按钮,就能把表格刷成含 EUR 的品种,想同时看 USD 和 EUR 就用逗号隔开写 "USD,EUR"。 下拉框换周期走的是 ON_CLICK_COMBOBOX_ITEM,落到 CProgram::ChangePeriod()。它先比对组合框 ID,再从列表读出时段文本转成 ENUM_TIMEFRAMES,接着对子图调用 Period() 并 ResetCharts(),随后重新取指标句柄和数值、刷新表格。表格第一列高亮某行则触发 ON_CLICK_LIST_ITEM,由 ChangeSymbol() 处理:重复点击会取消高亮,所以方法里先查 ID 再查行是否真被选中,通过后才用行索引去子图切品种,状态条同步显示全称。 键盘的 Up/Down/Home/End 也能切品种,这时来的是 CHARTEVENT_KEYDOWN,逻辑在前文已铺过。Show indicator 复选框发 ON_CLICK_CHECKBOX,ShowIndicator() 按句柄数组边界检查后,把唯一指标挂到子窗口索引 1;随机振荡的上下水平输入框默认 80/20,改完值后每 5 秒刷新一次,超限品种文字变蓝(超顶)或变红(破底)。 图表属性那组控件里,Date/Price scale 复选框直接开关对应属性并 ResetCharts() 拖到末尾;Chart scale 输入栏由 ON_CLICK_BUTTON 和 ON_END_EDIT 共管;Chart shift 按钮经 ChartShift() 拿图表 ID 设 CHART_SHIFT 做右缩进。下面这段节选能直接看出按钮事件的分流入口和 ChangePeriod 的刷新顺序。

MQL5 / C++
class="type">void CProgram::OnEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam)
  {
...
class=class="str">"cmt">//--- 按钮按下事件
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
     class=class="str">"cmt">//--- 数据请求
     if(RequestData(lparam))
       class="kw">return;
     class=class="str">"cmt">//---
     class="kw">return;
     }
...
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 改变时段                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::ChangePeriod(const class="type">long id)
  {
class=class="str">"cmt">//--- 检查元件 ID
   if(id!=m_timeframes.Id())
      class="kw">return(false);
class=class="str">"cmt">//--- 从组合框下拉列表中取值
   class="type">class="kw">string tf=m_timeframes.GetListViewPointer().SelectedItemText();
class=class="str">"cmt">//--- 根据索引取得图表指针
   m_sub_chart1.GetSubChartPointer(class="num">0).Period(StringToTimeframe(tf));
   m_sub_chart1.ResetCharts();
class=class="str">"cmt">//--- 显示进度
   m_progress_bar.Show();
   m_chart.Redraw();
class=class="str">"cmt">//--- 取得句柄和指标数据
   GetHandles();
   GetIndicatorValues();
class=class="str">"cmt">//--- 更新表格
   UpdateSymbolsTable();
class=class="str">"cmt">//--- 隐藏进度条
   m_progress_bar.Hide();
   m_chart.Redraw();
   class="kw">return(true);
  }

「切换品种时图表与状态栏怎么联动」

在自定义面板里点选交易品种,核心逻辑落在 CProgram::ChangeSymbol() 这个成员函数。它先核对触发事件的元件 ID 是否等于品种表格 m_table_symb 的 Id(),不对就直接返回 false,避免误触其他控件。 如果当前没有高亮行(SelectedItem() 返回 WRONG_VALUE),函数不会切换图表,而是在状态栏 0 号位写一句 'For Help, press F1' 并刷新,给操作者一个静默提示。 一旦拿到了合法的选中行索引,就把它存进 m_current_handle_index,再从表格第 0 列读出 symbol 字符串,接着用 m_sub_chart1.GetSubChartPointer(0).Symbol(symbol) 把子图 0 的交易品种改掉,调 ResetCharts() 重算,最后在状态栏 0 号位显示 SymbolInfoString(symbol, SYMBOL_DESCRIPTION) 拿到的完整品种描述并 Redraw()。 事件入口 OnEvent 里,当 id 等于 CHARTEVENT_CUSTOM+ON_CLICK_COMBOBOX_ITEM 时走的是另一支:调 ChangePeriod(lparam) 改周期,成功就 return,跟改品种是两套分支。外汇与贵金属品种切换涉及实时报价重连,点差跳变和滑点风险偏高,建议在模拟环境先跑通再上真仓。

MQL5 / C++
class="type">void CProgram::OnEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam)
  {
...
class=class="str">"cmt">//--- 在组合框中选择项目的事件
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_COMBOBOX_ITEM)
    {
      class=class="str">"cmt">//--- 改变时段
      if(ChangePeriod(lparam))
        class="kw">return;
      class=class="str">"cmt">//---
      class="kw">return;
    }
...
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 改变交易品种                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::ChangeSymbol(const class="type">long id)
  {
class=class="str">"cmt">//--- 检查元件 ID
   if(id!=m_table_symb.Id())
      class="kw">return(false);
class=class="str">"cmt">//--- 如果行没有突出显示,就退出
   if(m_table_symb.SelectedItem()==WRONG_VALUE)
    {
      class=class="str">"cmt">//--- 在进度条上显示交易品种的完整描述
      m_status_bar.SetValue(class="num">0,"For Help, press F1");
      m_status_bar.GetItemPointer(class="num">0).Update(true);
      class="kw">return(false);
    }
class=class="str">"cmt">//--- 保存句柄索引
   m_current_handle_index=m_table_symb.SelectedItem();
class=class="str">"cmt">//--- 取得交易品种
   class="type">class="kw">string symbol=m_table_symb.GetValue(class="num">0,m_current_handle_index);
class=class="str">"cmt">//--- 更新图表
   m_sub_chart1.GetSubChartPointer(class="num">0).Symbol(symbol);
   m_sub_chart1.ResetCharts();
class=class="str">"cmt">//--- 在进度条上显示完整的交易品种描述
   m_status_bar.SetValue(class="num">0,::SymbolInfoString(symbol,SYMBOL_DESCRIPTION));
   m_status_bar.GetItemPointer(class="num">0).Update(true);
   m_chart.Redraw();
   class="kw">return(true);
  }
class="type">void CProgram::OnEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam)
  {
...
class=class="str">"cmt">//--- 在列表/表格中选择一个项目的事件

◍ 用按键和点击切换品种与指标

面板交互的核心在于把图表事件分流到不同处理函数。下面这段事件分发逻辑,监听了自定义列表项点击与键盘按键两种信号:点击列表项时调用 ChangeSymbol 切品种,按键时调用 SelectingResultsUsingKeys 做结果选择,任一步成功就直接 return 不再向下走。 [CODE] if(id==CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM) { //--- 改变交易品种 if(ChangeSymbol(lparam)) return; //--- return; } //--- 点击按钮 if(id==CHARTEVENT_KEYDOWN) { //--- 使用按键选择结果 if(SelectingResultsUsingKeys(lparam)) return; //--- return; } [/CODE] 逐行看:第1行判断事件是否为自定义事件加列表项点击偏移;第4行尝试切换品种,lparam 携带了被点项的数据;第5行若切换成功立即退出避免重复处理。下方键盘分支同理,CHARTEVENT_KEYDOWN 触发后用 lparam 里的键值做结果选择。 指标的挂卸由 ShowIndicator 控制。它先核对元件 ID 与句柄索引边界——m_current_handle_index 若小于 0 或大于 ArraySize(m_handles)-1 就直接返回 true 不做操作,这个边界检查能避免越界访问导致脚本报错。 [CODE] bool CProgram::ShowIndicator(const long id) { if(id!=m_show_indicator.Id()) return(false); int handles_total=::ArraySize(m_handles);

if(m_current_handle_index<0m_current_handle_index>handles_total-1)

return(true); long sub_chart_id=m_sub_chart1.GetSubChartPointer(0).GetInteger(OBJPROP_CHART_ID); int subwindow =1; if(m_show_indicator.IsPressed()) { ::ChartIndicatorAdd(sub_chart_id,subwindow,m_handles[m_current_handle_index]); } else { ::ChartIndicatorDelete(sub_chart_id,subwindow,ChartIndicatorName(sub_chart_id,subwindow,0)); } m_chart.Redraw(); return(true); } [/CODE] 关键行:sub_chart_id 从子图对象取真实图表 ID;subwindow 硬编码为 1 表示指标加到第一个子窗口;IsPressed 为真就 ChartIndicatorAdd 挂上当前句柄,否则用 ChartIndicatorName 取名下标 0 的指标名做 ChartIndicatorDelete 卸载。最后 Redraw 刷新。 开 MT5 把这段塞进你的面板类,按一下按钮看子窗口指标挂卸是否符合预期;外汇和贵金属波动剧烈,这类交互工具只降低操作延迟,不消除高风险。

MQL5 / C++
if(id==CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM)
  {
  class=class="str">"cmt">//--- 改变交易品种
  if(ChangeSymbol(lparam))
    class="kw">return;
  class=class="str">"cmt">//---
  class="kw">return;
  }
class=class="str">"cmt">//--- 点击按钮
if(id==CHARTEVENT_KEYDOWN)
  {
  class=class="str">"cmt">//--- 使用按键选择结果
  if(SelectingResultsUsingKeys(lparam))
    class="kw">return;
  class=class="str">"cmt">//---
  class="kw">return;
  }
class="type">bool CProgram::ShowIndicator(const class="type">long id)
  {
  if(id!=m_show_indicator.Id())
    class="kw">return(false);
  class="type">int handles_total=::ArraySize(m_handles);
  if(m_current_handle_index<class="num">0 || m_current_handle_index>handles_total-class="num">1)
    class="kw">return(true);
  class="type">long sub_chart_id=m_sub_chart1.GetSubChartPointer(class="num">0).GetInteger(OBJPROP_CHART_ID);
  class="type">int subwindow =class="num">1;
  if(m_show_indicator.IsPressed())
    {
    ::ChartIndicatorAdd(sub_chart_id,subwindow,m_handles[m_current_handle_index]);
    }
  else
    {
    ::ChartIndicatorDelete(sub_chart_id,subwindow,ChartIndicatorName(sub_chart_id,subwindow,class="num">0));
    }
  m_chart.Redraw();
  class="kw">return(true);
  }

用事件回调接管图表缩放开关

面板上的复选框和按钮不是装饰,点下去必须触发底层图表重绘。OnEvent 里先判断 id 是否等于 CHARTEVENT_CUSTOM+ON_CLICK_CHECKBOX,命中后才去查 ShowIndicator(lparam) 的返回值,避免把无关界面事件带进指标逻辑。 时间轴与价格轴的显隐由 DateScale / PriceScale 两个方法分别接管。核心只有两行:GetSubChartPointer(0).DateScale(m_date_scale.IsPressed()) 把按钮按压态透传给子图 0,再调 ResetCharts() 清掉旧比例尺缓存,最后 m_chart.Redraw() 才会让 MT5 主图刷新。 注意 GetSubChartPointer 的索引写死为 0,意味着只控制第一个子图表。若你挂了第二层副图(索引 1)却没改这里,点面板不会生效。外汇与贵金属波动剧烈,这类界面联动 bug 可能在快速行情中让你误判缩放状态,实盘前务必在策略测试器里点一遍验证。

MQL5 / C++
class="type">void CProgram::OnEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- 点击了 "Check box" 元件
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_CHECKBOX)
    {
class=class="str">"cmt">//--- 如果点击了 "Show indicator" 复选框
     if(ShowIndicator(lparam))
       class="kw">return;
class=class="str">"cmt">//---
     class="kw">return;
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 时间缩放的显示                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::DateScale(const class="type">long id)
  {
class=class="str">"cmt">//--- 检查元件 ID
   if(id!=m_date_scale.Id())
     class="kw">return(false);
class=class="str">"cmt">//--- 根据索引取得图表指针
   m_sub_chart1.GetSubChartPointer(class="num">0).DateScale(m_date_scale.IsPressed());
   m_sub_chart1.ResetCharts();
class=class="str">"cmt">//--- 更新图表
   m_chart.Redraw();
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 价格缩放的显示                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::PriceScale(const class="type">long id)
  {
class=class="str">"cmt">//--- 检查元件 ID
   if(id!=m_price_scale.Id())
     class="kw">return(false);
class=class="str">"cmt">//--- 根据索引取得图表指针
   m_sub_chart1.GetSubChartPointer(class="num">0).PriceScale(m_price_scale.IsPressed());
   m_sub_chart1.ResetCharts();
class=class="str">"cmt">//--- 更新图表
   m_chart.Redraw();
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 图表缩放                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::ChartScale(const class="type">long id)
  {
class=class="str">"cmt">//--- 检查元件 ID
   if(id!=m_chart_scale.Id())

「子图缩放与偏移的事件接线」

这段逻辑把界面上的缩放滑块、按钮和输入框都引到了同一个 ChartScale 处理函数。注意判断条件是 (int)m_chart_scale.GetValue() != m_sub_chart1.GetSubChartPointer(0).Scale(),也就是主图控件值和 0 号子图当前 Scale 不一致时才写回,避免每帧无谓重绘。 OnEvent 里对 CHARTEVENT_CUSTOM+ON_CLICK_BUTTON 和 CHARTEVENT_CUSTOM+ON_END_EDIT 都调了 ChartScale(lparam),说明点按钮、改输入栏结束编辑都会触发子图同步缩放,但函数内只判断了 id 匹配就 return,没有做额外分支——事件路由很薄,重活都在 ChartScale 里。 ChartShift 则是另一路:先比对 id 是否等于 m_chart_shift.Id(),不符直接返回 false;通过后用 OBJPROP_CHART_ID 拿到子图 ID,再 ChartSetInteger(sub_chart_id, CHART_SHIFT, true) 把右侧缩进打开,最后 ResetCharts()。外汇和贵金属图表多子图联动时,这种偏移设置容易导致视觉错位,建议开 MT5 挂脚本验证缩进像素表现。

MQL5 / C++
  class="kw">return(false);
class=class="str">"cmt">//--- 设置缩放
  if((class="type">int)m_chart_scale.GetValue()!=m_sub_chart1.GetSubChartPointer(class="num">0).Scale())
    m_sub_chart1.GetSubChartPointer(class="num">0).Scale((class="type">int)m_chart_scale.GetValue());
class=class="str">"cmt">//--- 更新
  m_chart.Redraw();
  class="kw">return(true);
  }
class="type">void CProgram::OnEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam)
  {
...
class=class="str">"cmt">//--- 按钮点击事件
  if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
    {
    class=class="str">"cmt">//--- 图表缩放
    if(ChartScale(lparam))
      class="kw">return;
    class=class="str">"cmt">//---
    class="kw">return;
    }
class=class="str">"cmt">//--- 输入栏位中修改数值结束的事件
  if(id==CHARTEVENT_CUSTOM+ON_END_EDIT)
    {
    class=class="str">"cmt">//--- 图表缩放
    if(ChartScale(lparam))
      class="kw">return;
    class=class="str">"cmt">//---
    class="kw">return;
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 图表的偏移                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::ChartShift(const class="type">long id)
  {
class=class="str">"cmt">//--- 检查元件 ID
  if(id!=m_chart_shift.Id())
    class="kw">return(false);
class=class="str">"cmt">//--- 取得图表 ID
  class="type">long sub_chart_id=m_sub_chart1.GetSubChartPointer(class="num">0).GetInteger(OBJPROP_CHART_ID);
class=class="str">"cmt">//--- 设置图表在右侧的缩进
  ::ChartSetInteger(sub_chart_id,CHART_SHIFT,true);
  m_sub_chart1.ResetCharts();
  class="kw">return(true);
  }

◍ 把 GUI 按钮接进 CTrade 下单逻辑

给 EA 加交易功能,最省事的做法是直接引用标准库的 Trade.mqh,用里面的 CTrade 类实例来发单。比起自己写下单协议,这套封装能让你在 GUI 按钮回调里两行代码就发出市价单,外汇与贵金属保证金交易波动剧烈,异步模式配合滑点容差能降低卡单概率。 实例化后第一件事是切异步:m_trade.SetAsyncMode(true) 让程序发完指令就继续跑,不阻塞等成交回执;滑点用 SetDeviationInPoints(INT_MAX) 先放开到最大,实盘里建议改成具体点数,否则极端行情可能以不利偏差成交。 Buy / Sell 按钮的回调只取两个变量:手数从 Lot 输入栏用 NormalizeDouble 四舍五入到 2 位,交易品种从当前子图指针的 Symbol() 拿。CTrade::Buy(lot, symbol) 只传这两个参数就能开仓,最小可用条件就齐了。 CTrade 本身没有「一键全平」或「平某个品种」的现成方法,得自己包一层。传入品种名就只平该品种,不传就平所有仓位;按钮绑定后务必弹确认窗,避免误点把账户清光。Positions 表格里每行第一列嵌了带叉按钮,但表格滚动条也会发同 ID 的 ON_CLICK_BUTTON 事件,要在 sparam 里过滤掉 "scroll" 字符串再处理。 仓位表不能下了单不刷新。用 IsLastDealTicket() 比对上次保存的时间和末笔成交单号,只在历史真正新增时才重绘表格——这样既不用每次都拉全量历史,也不会漏掉新平仓导致的行变动。

MQL5 / C++
class="macro">#include <Trade\Trade.mqh>
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
   CTrade            m_trade;
  };
CProgram::CProgram(class="type">void)
  {
...  m_trade.SetAsyncMode(true);
   m_trade.SetDeviationInPoints(INT_MAX);
  }
class="type">bool CProgram::OnBuy(const class="type">long id)
  {
   if(id!=m_buy.Id())
      class="kw">return(false);
   class="type">class="kw">double lot     =::NormalizeDouble((class="type">class="kw">double)m_lot.GetValue(),class="num">2);
   class="type">class="kw">string symbol =m_sub_chart1.GetSubChartPointer(class="num">0).Symbol();
   m_trade.Buy(lot,symbol);
   class="kw">return(true);
  }

卖出按钮与一键清仓的事件回调

面板上的卖出按钮被点击时,OnSell 先比对元件 ID,不匹配直接返回 false,避免误触发。匹配后从 m_lot 取手数并保留两位小数,再从子图 0 拿到当前交易品种,最后调用 m_trade.Sell 市价开空。 手数归一化用 NormalizeDouble((double)m_lot.GetValue(),2),意味着面板手数输入会被强制截断到 0.01 精度,0.123 会变成 0.12,实盘前要在面板里确认最小手数步长是否也是 2 位。 CloseAllPosition 用 PositionsTotal 倒序遍历,按 ticket 逐个 PositionClose。传入 symbol 非空时只平该品种,空字符串则清掉账户下所有品种仓位;平不掉就 Print 错误码,不中断循环。 OnCloseAllPositions 负责把『全部平仓』按钮的 ID 和 m_close_all 比对,通过后才进对话框逻辑。外汇与贵金属杠杆高,市价一键清仓在滑点行情中可能以偏离报价成交,验证时建议先开 0.01 手模拟单跑这套回调。

MQL5 / C++
class="type">bool CProgram::OnSell(const class="type">long id)
  {
class=class="str">"cmt">//--- 检查元件 ID
   if(id!=m_sell.Id())
      class="kw">return(false);
class=class="str">"cmt">//--- 用于开启仓位的交易量和交易品种
   class="type">class="kw">double lot     =::NormalizeDouble((class="type">class="kw">double)m_lot.GetValue(),class="num">2);
   class="type">class="kw">string symbol =m_sub_chart1.GetSubChartPointer(class="num">0).Symbol();
class=class="str">"cmt">//--- 开启仓位
   m_trade.Sell(lot,symbol);
   class="kw">return(true);
  }
class="type">bool CProgram::CloseAllPosition(const class="type">class="kw">string symbol="")
  {
class=class="str">"cmt">//--- 检查仓位中是否有指定属性
   class="type">int total=::PositionsTotal();
   for(class="type">int i=total-class="num">1; i>=class="num">0; i--)
     {
      class=class="str">"cmt">//--- 选择一个仓位
      class="type">class="kw">string pos_symbol=::PositionGetSymbol(i);
      class=class="str">"cmt">//--- 如果根据交易品种关闭
      if(symbol!="")
         if(symbol!=pos_symbol)
            class="kw">continue;
      class=class="str">"cmt">//--- 取得编号
      class="type">class="kw">ulong position_ticket=::PositionGetInteger(POSITION_TICKET);
      class=class="str">"cmt">//--- 重置最近的错误
      ::ResetLastError();
      class=class="str">"cmt">//--- 如果没有关闭仓位,做通知
      if(!m_trade.PositionClose(position_ticket))
         ::Print(__FUNCTION__,": > 当关闭仓位时出错: ",::GetLastError());
     }
class=class="str">"cmt">//---
   class="kw">return(true);
  }
class="type">bool CProgram::OnCloseAllPositions(const class="type">long id)
  {
class=class="str">"cmt">//--- 检查元件 ID
   if(id!=m_close_all.Id())
      class="kw">return(false);
class=class="str">"cmt">//--- 对话框窗口

「按品种平仓前的点击拦截与确认」

在持仓面板里做按品种平仓,第一步不是直接发单,而是先确认这次点击事件落在正确的控件上。代码里用元件 ID 比对,若不是持仓表对象直接返回 false,避免误触其他界面元素触发平仓逻辑。 滚动条按钮的点击描述里会带 "scroll" 字样,用 StringFind 检索到就退出,防止拖动滚动条时被识别成平仓指令。若当前 PositionsTotal() 小于 1,即账户没有任何持仓,也直接返回,省掉弹窗。 真正进入平仓流程前,要从 desc 字符串按 "_" 分割取出行索引,再用 m_table_positions.GetValue(0, row_index) 拿到具体交易品种。之后弹出的 MessageBox 带 MB_YESNO|MB_ICONWARNING,用户点 IDYES 才调用 CloseAllPosition(symbol) 平掉该品种全部仓位。外汇与贵金属杠杆高,这类一键平仓必须加确认,否则滑点叠加波动可能造成超预期回撤。 下面这段是核心确认与派发逻辑,注意StringFind对"scroll"的拦截和symbol的提取方式: int mb_id=::MessageBox("您是否想要关闭\n交易品种的所有仓位 - "+symbol+"?","关闭仓位",MB_YESNO|MB_ICONWARNING); if(mb_id==IDYES) CloseAllPosition(symbol);

MQL5 / C++
class="type">int mb_id=::MessageBox("Are you sure you want to close \nall positions?","Close positions",MB_YESNO|MB_ICONWARNING);
class=class="str">"cmt">//--- 关闭仓位
if(mb_id==IDYES)
   CloseAllPosition();
class=class="str">"cmt">//---
class="kw">return(true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 关闭指定交易品种的所有仓位                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::OnCloseSymbolPositions(const class="type">long id,const class="type">class="kw">string desc)
  {
class=class="str">"cmt">//--- 检查元件 ID
  if(id!=m_table_positions.Id())
     class="kw">return(false);
class=class="str">"cmt">//--- 如果是点击在滚动条按钮上,就退出
  if(::StringFind(desc,"scroll",class="num">0)!=WRONG_VALUE)
     class="kw">return(false);
class=class="str">"cmt">//--- 如果没有仓位就退出
  if(::PositionsTotal()<class="num">1)
     class="kw">return(true);
class=class="str">"cmt">//---从字符串中展开数据
  class="type">class="kw">string str_elements[];
  class="type">class="kw">ushort sep=::StringGetCharacter("_",class="num">0);
  ::StringSplit(desc,sep,str_elements);
class=class="str">"cmt">//--- 取得索引和交易品种 
  class="type">int     row_index =(class="type">int)str_elements[class="num">1];
  class="type">class="kw">string symbol    =m_table_positions.GetValue(class="num">0,row_index);
class=class="str">"cmt">//--- 对话框窗口
  class="type">int mb_id=::MessageBox("您是否想要关闭\n交易品种的所有仓位 -  "+symbol+"?","关闭仓位",MB_YESNO|MB_ICONWARNING);
class=class="str">"cmt">//--- 关闭一个指定交易品种的所有仓位
  if(mb_id==IDYES)
     CloseAllPosition(symbol);
class=class="str">"cmt">//---
  class="kw">return(true);
  }

◍ 面板按钮如何驱动下单与仓位管理

在 MT5 的自定义面板里,按钮点击本质是图表自定义事件。引擎把 CHARTEVENT_CUSTOM 加上按钮自己的 ID 偏移(如 ON_CLICK_BUTTON)作为 id 传入 OnEvent,靠 lparam 区分是哪个控件被按了。 事件分发写得非常直白:先判 id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON,进去后依次试 OnBuy(lparam)OnSell(lparam)OnCloseAllPositions(lparam)OnCloseSymbolPositions(lparam,sparam),谁先返回 true 就直接 return,不再往下走。这意味着同一帧里买入和清仓不会打架,优先级靠代码顺序定。 为了不在每次 OnEvent 里全量扫历史,类里挂了 datetime m_last_deal_timeulong m_last_deal_ticket 两个私有成员,构造时分别初始化成 NULLWRONG_VALUEIsLastDealTicket 的入口有个硬判断:if(!::HistorySelect(m_last_deal_time,UINT_MAX)) 就退出。也就是说,如果从 m_last_deal_time 到现在拿不到历史,后面的新成交检测直接跳过——实盘里如果遇到经纪商历史同步延迟,这段代码可能静默漏掉一两笔成交,值得在日志里补一行。 外汇与贵金属杠杆高,这类一键平仓逻辑若 lparam 映射错控件,可能误平无关品种仓位,上 MT5 跑之前先把按钮 ID 打印出来核对。

MQL5 / C++
class="type">void CProgram::OnEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam)
  {
  class=class="str">"cmt">//--- 按钮点击事件
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
    {
      class=class="str">"cmt">//--- 买入
      if(OnBuy(lparam))
        class="kw">return;
      class=class="str">"cmt">//--- 卖出
      if(OnSell(lparam))
        class="kw">return;
      class=class="str">"cmt">//--- 关闭所有仓位
      if(OnCloseAllPositions(lparam))
        class="kw">return;
      class=class="str">"cmt">//--- 关闭指定交易品种的所有仓位
      if(OnCloseSymbolPositions(lparam,sparam))
        class="kw">return;
      class=class="str">"cmt">//---
      class="kw">return;
    }
  }
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
   class=class="str">"cmt">//--- 时间和最后检查的交易的单号
   class="type">class="kw">datetime        m_last_deal_time;
   class="type">class="kw">ulong           m_last_deal_ticket;
class="kw">private:
   class=class="str">"cmt">//--- 在历史中检查新的交易
   class="type">bool            IsLastDealTicket(class="type">void);
  };
CProgram::CProgram(class="type">void) : m_last_deal_time(NULL),
                           m_last_deal_ticket(WRONG_VALUE)
  {
  }
class="type">bool CProgram::IsLastDealTicket(class="type">void)
  {
  class=class="str">"cmt">//--- 如果没有得到历史,就退出
   if(!::HistorySelect(m_last_deal_time,UINT_MAX))
    class="kw">return(false);
  }

用历史成交单号拦掉重复推送

在 EA 里监听交易事件时,最怕同一笔成交被历史回放反复触发通知。上面这段逻辑的核心,是先拿 HistoryDealsTotal() 取历史成交总数,再从最新一笔往回遍历,用已记录的 m_last_deal_ticket 做比对。 遍历里第一句用 HistoryDealGetTicket(i) 拿当前下标对应的成交单号;若它等于上次记下的单号,直接 return(false) 表示没有新成交。若不等,就通过 HistoryDealGetInteger(deal_ticket, DEAL_TIME) 取出该笔成交时间,写进 m_last_deal_time 与 m_last_deal_ticket,然后 return(true) 告知上层有新交易。 OnTradeEvent() 中只要 IsLastDealTicket() 返回 true,就调 InitializePositionsTable() 重算仓位表。外汇与贵金属杠杆高,这类重复触发若没拦住,可能在几秒内对同一个信号发多次提醒,干扰盯盘判断。

MQL5 / C++
  class="kw">return(false);
class=class="str">"cmt">//--- 在取得的列表中得到交易的数量
   class="type">int total_deals=::HistoryDealsTotal();
class=class="str">"cmt">//--- 遍历在获得的列表中的全部交易,从最新的到最开始的
   for(class="type">int i=total_deals-class="num">1; i>=class="num">0; i--)
     {
     class=class="str">"cmt">//--- 取得交易编号
      class="type">class="kw">ulong deal_ticket=::HistoryDealGetTicket(i);
     class=class="str">"cmt">//--- 如果单号匹配,就退出
      if(deal_ticket==m_last_deal_ticket)
        class="kw">return(false);
     class=class="str">"cmt">//--- 如果单号没有匹配,就进行通知
     else
       {
        class="type">class="kw">datetime deal_time=(class="type">class="kw">datetime)::HistoryDealGetInteger(deal_ticket,DEAL_TIME);
       class=class="str">"cmt">//--- 记住最新交易的时间和单号
       m_last_deal_time   =deal_time;
       m_last_deal_ticket =deal_ticket;
       class="kw">return(true);
       }
     }
class=class="str">"cmt">//--- 另一个交易品种的订单
   class="kw">return(false);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 交易操作事件                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::OnTradeEvent(class="type">void)
  {
class=class="str">"cmt">//--- 如果有新的交易
   if(IsLastDealTicket())
     {
     class=class="str">"cmt">//--- 初始化仓位表格
      InitializePositionsTable();
     }
  }

「一点提醒」

这套带 GUI 的手动交易面板已经把底层类拆干净了:Program.mqh 管逻辑,CreateGUI.mqh 管界面,Table.mqh 和 Keys.mqh 是更新过的控件基类,直接丢进 MQL5\Experts\TradePanel 就能编译出 TradePanel.mq5 用。不想碰代码的,市场里现成的 Trading Exposure 类应用也能跑同样的暴露度监控。 不过评论区有人反馈压缩包缺了 TimeCounter.mqh,导致 CreateGUI.mqh 里 AddItem 参数计数报错编译不过;真要复现,得自己补这个计时库或从回帖里找别人分享的版本。外汇和贵金属杠杆高,这类面板只是把仓位和计算结果可视化,不替你判断方向,实盘前务必在策略测试器里跑一遍附件代码确认链路通。

常见问题

面板初始化只画了空行,必须在 GUI 创建完成后立刻调用灌入函数把当前品种和持仓写进对应字段,否则要等下次刷新才有数。
行数按最大可能持仓写死,若少于实际持仓,多出的单子没行可写直接漏显示;建议预留比常规仓位多一倍的行数。
可以。小布能接入你的品种页做 AIGC 诊断,自动比对面板字段与真实持仓,发现行数不足或刷新停滞会直接提示你。
不会,只要把品种表、状态栏、持仓面板分到不同周期计时器并错开触发,表格刷新节奏可控,MT5 主线程占用很低。
状态栏偏全局提示、品种表偏行情切换,落点分开能避免互相覆盖写入,也方便单独调某一项的刷新频率。