图形界面 X: 标准图表控件 (集成编译 4)·综合运用
📊

图形界面 X: 标准图表控件 (集成编译 4)·综合运用

(3/3)·从类封装到引擎减负,一篇走完库里最后这块拼图,顺手压住 CPU 占用

实战向 第 3/3 篇
很多人把子图表当静态贴图用,鼠标一拖就卡顿,还以为是终端不行。其实缺的是带水平滚动和事件分流的控件封装,硬写重复代码只会把负载堆给主线程。

◍ 自绘图表里的水平拖拽滚动怎么落地

在 MT5 自绘图形库里,水平滚动不是靠终端自带平移,而是用一个 CStandardChart 类把鼠标左键拖拽转成偏移量。类里挂了三个私有整型变量:m_prev_x 记上次按下时的基准坐标,m_new_x_point 和 m_prev_new_x_point 分别存当前与上一帧的新偏移,靠它们算拖拽距离。 核心逻辑在 HorizontalScroll(void) 里。先判 m_x_scroll_mode,关闭就直接 return;左键按下时,若 m_prev_x==0 就把它设成 鼠标X + 上一次偏移,并锁住窗口防止其他元素抢事件,再把指针按鼠标位置减间距移过去并设为可见。 松开左键走 else 分支:m_prev_x 清零、窗口解锁、指针 Hide()。有个细节——代码里写 if(m_new_x_point>0) return;,意味着偏移量为正(图表向右拖)时直接退出不应用,只在负值区间真正滚动,这点和常规预期相反,调参时要注意。 下面这段是原文里的水平滚动函数骨架,逐行拆一下关键行: // 若水平滚动模式未开,直接离开 if(!m_x_scroll_mode) return; // 鼠标左键按下才进入拖拽 if(m_mouse.LeftButtonState()) { // 首次按下记录基准 X if(m_prev_x==0) { m_prev_x =m_mouse.X()+m_prev_new_x_point; m_new_x_point =m_prev_new_x_point; } else m_new_x_point=m_prev_x-m_mouse.X(); // 锁窗并标记当前元素 if(!m_wnd.IsLocked()) { m_wnd.IsLocked(true); m_wnd.IdActivatedElement(CElement::Id()); } // 移动并显示滚动指针 int l_x=m_mouse.X()-m_x_scroll.XGap(); int l_y=m_mouse.Y()-m_x_scroll.YGap(); m_x_scroll.Moving(l_x,l_y); m_x_scroll.Show(); m_x_scroll.IsVisible(true); } 外汇与贵金属图表叠加自绘控件时,这种拖拽锁窗逻辑可能在快速滑动时丢帧,建议在 MT5 策略测试器外的实盘图表手动拖几下验证手感,高频波动品种尤需留意高风险。

MQL5 / C++
class="type">void CStandardChart::HorizontalScroll(class="type">void)
  {
class=class="str">"cmt">//--- 离开, 如果图表水平滚动被禁用
  if(!m_x_scroll_mode)
    class="kw">return;
class=class="str">"cmt">//--- 如果鼠标按钮被按下
  if(m_mouse.LeftButtonState())
    {
    class=class="str">"cmt">//--- 保存当前光标的 X 坐标
    if(m_prev_x==class="num">0)
      {
      m_prev_x      =m_mouse.X()+m_prev_new_x_point;
      m_new_x_point =m_prev_new_x_point;
      }
    else
      m_new_x_point=m_prev_x-m_mouse.X();
    class=class="str">"cmt">//--- 阻塞表单
    if(!m_wnd.IsLocked())
      {
      m_wnd.IsLocked(true);
      m_wnd.IdActivatedElement(CElement::Id());
      }
    class=class="str">"cmt">//--- 更新指针坐标, 并令其可见
    class="type">int l_x=m_mouse.X()-m_x_scroll.XGap();
    class="type">int l_y=m_mouse.Y()-m_x_scroll.YGap();
    m_x_scroll.Moving(l_x,l_y);
    class=class="str">"cmt">//--- 显示指针
    m_x_scroll.Show();
    class=class="str">"cmt">//--- 设置可视标志
    m_x_scroll.IsVisible(true);
    }
  else
    {
    m_prev_x=class="num">0;
    class=class="str">"cmt">//--- 解锁表单
    if(m_wnd.IdActivatedElement()==CElement::Id())
      {
      m_wnd.IsLocked(class="kw">false);
      m_wnd.IdActivatedElement(WRONG_VALUE);
      }
    class=class="str">"cmt">//--- 隐藏指针
    m_x_scroll.Hide();
    class=class="str">"cmt">//--- 设置可视标志
    m_x_scroll.IsVisible(class="kw">false);
    class="kw">return;
    }
class=class="str">"cmt">//--- 离开, 如果为正数值
  if(m_new_x_point>class="num">0)
    class="kw">return;
class=class="str">"cmt">//--- 保存当前位置
  m_prev_new_x_point=m_new_x_point;
class=class="str">"cmt">//--- 应用于所有图表
  class="type">int symbols_total=SubChartsTotal();
class=class="str">"cmt">//--- 禁用自动滚动和自右边界平移
  for(class="type">int i=class="num">0; i<symbols_total; i++)
    {

「子图表的自动滚动与坐标复位」

多子图面板最怕用户手滑开了自动滚动或平移,主图节奏一下就被带偏。代码里先逐个子图查 CHART_AUTOSCROLL 和 CHART_SHIFT,只要为真就强制置 false,把视图锁死在程序控制范围内。 复位动作分两层:ResetCharts() 先按 SubChartsTotal() 拿到子图数量,挨个 ChartNavigate 到 CHART_END;随后调 ZeroHorizontalScrollVariables() 把 m_prev_x、m_new_x_point、m_prev_new_x_point 三个水平滚动辅助变量清 0,避免上一次拖拽的偏移被继承。 事件侧用 ON_CLICK_SUB_CHART (28) 标识子图点击,由 OnClickSubChart(const string clicked_object) 承接。外汇与贵金属图表多周期联动时,这类复位能降低误触导致的视觉错位概率,但 MT5 环境参数复杂,实盘前请在模拟盘验证。

MQL5 / C++
if(::ChartGetInteger(m_sub_chart_id[i],CHART_AUTOSCROLL))
   ::ChartSetInteger(m_sub_chart_id[i],CHART_AUTOSCROLL,class="kw">false);
if(::ChartGetInteger(m_sub_chart_id[i],CHART_SHIFT))
   ::ChartSetInteger(m_sub_chart_id[i],CHART_SHIFT,class="kw">false);
}
class=class="str">"cmt">//--- 重置最后的错误
 ::ResetLastError();
class=class="str">"cmt">//--- 平移图表
 for(class="type">int i=class="num">0; i<symbols_total; i++)
  if(!::ChartNavigate(m_sub_chart_id[i],CHART_END,m_new_x_point))
    ::Print(__FUNCTION__,"> 错误: ",::GetLastError());
}
class CStandardChart : class="kw">public CElement
 {
class="kw">public:
  class=class="str">"cmt">//--- 重置图表
  class="type">void                ResetCharts(class="type">void);
  class=class="str">"cmt">//---
class="kw">private:
  class=class="str">"cmt">//--- 重置水平滚动变量
  class="type">void                ZeroHorizontalScrollVariables(class="type">void);
  };
class="type">void CStandardChart::ResetCharts(class="type">void)
 {
  class="type">int sub_charts_total=SubChartsTotal();
  for(class="type">int i=class="num">0; i<sub_charts_total; i++)
   ::ChartNavigate(m_sub_chart_id[i],CHART_END);
class=class="str">"cmt">//--- 重置用于图表水平滚动的辅助变量
  ZeroHorizontalScrollVariables();
  }
class="type">void CStandardChart::ZeroHorizontalScrollVariables(class="type">void)
 {
  m_prev_x           =class="num">0;
  m_new_x_point      =class="num">0;
  m_prev_new_x_point =class="num">0;
  }
class="macro">#define ON_CLICK_SUB_CHART(class="num">28) class=class="str">"cmt">// 点击子图表
class CStandardChart : class="kw">public CElement
 {
class="kw">private:
  class=class="str">"cmt">//--- 处理子图表按下
  class="type">bool                OnClickSubChart(class="kw">const class="type">class="kw">string clicked_object);
  };

子图点击与按日期跳转的实现细节

在自定义面板里,子图按钮的点击响应不能靠全局事件瞎抓。OnClickSubChart 先拿 StringFind 比对对象名是否带 _sub_chart_ 前缀,不匹配直接 return false,避免主图和其他控件误触发。 拿到对象名后,用 IdFromObjectName 和 IndexFromObjectName 拆出标识符与组索引。只有 id 和本元素 Id() 一致才继续,随后 EventChartCustom 把 ON_CLICK_SUB_CHART 事件抛出去,附带组和品种名,主逻辑在别处接这个自定义事件即可。 跳日期则是另一套活。SubChartNavigate 先取当前日期(截断到天)和选定日期,若选定日 >= 今天,直接 ChartNavigate(CHART_END) 回最新价,并 ResetCharts 同步所有子图。 若选定日在过去,用 Bars() 算两日之间的柱数,再读 CHART_VISIBLE_BARS,结合当天已走秒数换算 bars_today(PeriodSeconds 除出来加 2 根余量)。最终缩进量 m_new_x_point = -((bars_total-visible_bars)+bars_today),负数表示从右边界往左推多少根,ChartNavigate 带这个偏移量就能把画面定到历史那天。外汇与贵金属波动剧烈,这类跳转只解决视图定位,不预示任何方向。

MQL5 / C++
class="type">bool CStandardChart::OnClickSubChart(class="kw">const class="type">class="kw">string clicked_object)
  {
  if(::StringFind(clicked_object,CElement::ProgramName()+"_sub_chart_",class="num">0)<class="num">0)
     class="kw">return(class="kw">false);
  class="type">int id=CElement::IdFromObjectName(clicked_object);
  if(id!=CElement::Id())
     class="kw">return(class="kw">false);
  class="type">int group_index=CElement::IndexFromObjectName(clicked_object);
  ::EventChartCustom(m_chart_id,ON_CLICK_SUB_CHART,CElement::Id(),group_index,m_sub_chart_symbol[group_index]);
  class="kw">return(true);
  }
class CStandardChart : class="kw">public CElement
  {
class="kw">public:
   class="type">void        SubChartNavigate(class="kw">const class="type">class="kw">datetime date);
  };
class="type">void CStandardChart::SubChartNavigate(class="kw">const class="type">class="kw">datetime date)
  {
  class="type">class="kw">datetime current_date  =::StringToTime(::TimeToString(::TimeCurrent(),TIME_DATE));
  class="type">class="kw">datetime selected_date =date;
  ::ChartSetInteger(m_chart_id,CHART_AUTOSCROLL,class="kw">false);
  ::ChartSetInteger(m_chart_id,CHART_SHIFT,class="kw">false);
  if(selected_date>=current_date)
     {
     ::ChartNavigate(m_chart_id,CHART_END);
     ResetCharts();
     class="kw">return;
     }
  class="type">int  bars_total     =::Bars(::Symbol(),::Period(),selected_date,current_date);
  class="type">int  visible_bars   =(class="type">int)::ChartGetInteger(m_chart_id,CHART_VISIBLE_BARS);
  class="type">long seconds_today =::TimeCurrent()-current_date;
  class="type">int  bars_today     =class="type">int(seconds_today/::PeriodSeconds())+class="num">2;
  m_prev_new_x_point=m_new_x_point=-((bars_total-visible_bars)+bars_today);
  ::ChartNavigate(m_chart_id,CHART_END,m_new_x_point);
  class="type">int sub_charts_total=SubChartsTotal();
  for(class="type">int i=class="num">0; i<sub_charts_total; i++)
     {
     ::ChartSetInteger(m_sub_chart_id[i],CHART_AUTOSCROLL,class="kw">false);
     ::ChartSetInteger(m_sub_chart_id[i],CHART_SHIFT,class="kw">false);

◍ 按日期回滚子图可见柱线

在多品种监控面板里,子图表往往需要根据用户选中的历史日期自动滚回去,而不是永远停在最新一根。上面这段代码干的就是这件事:先算出自 selected_date 到 current_date 的总柱数,再读当前图表的可见柱数,最后用 PeriodSeconds 把当天已过的秒数换算成大约的当日柱数并加 2 根缓冲。 关键在 m_prev_new_x_point 与 m_new_x_point 的赋值:用 (bars_total - visible_bars) 补上被滚出去的隐藏柱,再叠上 bars_today,得到相对图表右边界的负向偏移量。ChartNavigate 带 CHART_END 和这个负值,就能把视窗精准推到选中日附近。 实盘里若 visible_bars 因窗口缩放频繁变动,重算后直接 Navigate 可能出现轻微跳动;建议在调用前用 ChartGetInteger 再取一次 CHART_WIDTH_IN_BARS 做交叉校验。外汇与贵金属波动剧烈,这类自动定位仅辅助看盘,不改变任何交易决策概率。

MQL5 / C++
   class=class="str">"cmt">//--- 获取自指定日期的柱线数量
   bars_total   =::Bars(m_sub_chart[i].Symbol(),(ENUM_TIMEFRAMES)m_sub_chart[i].Period(),selected_date,current_date);
   visible_bars =(class="type">int)::ChartGetInteger(m_sub_chart_id[i],CHART_VISIBLE_BARS);
   bars_today   =class="type">int(seconds_today/::PeriodSeconds((ENUM_TIMEFRAMES)m_sub_chart[i].Period()))+class="num">2;
   class=class="str">"cmt">//--- 距图表右边界的缩进
   m_prev_new_x_point=m_new_x_point=-((bars_total-visible_bars)+bars_today);
   ::ChartNavigate(m_sub_chart_id[i],CHART_END,m_new_x_point);
   }
  }

「用选项卡把多货币子图塞进一个面板」

做测试时,直接拿前一篇的 EA 改最省事:把主菜单、状态栏和选项卡之外的控件全删掉,让每个选项卡绑定一组子图表。第一组放 EUR、第二组 GBP、第三组 AUD、第四组 CAD、第五组 JPY,这样切换标签就能看不同货币的相对走势。 工作区右侧要留 173 像素的硬缩进,专门摆属性控件——时间刻度、价格刻度、周期切换、日历导航都塞这里。表单缩放时子图自动跟着变,不用手动重排。 子图默认关了水平滚动,真要横向拖,用 CStandardChart::XScrollMode() 打开;往组里加品种靠 CStandardChart::AddSubChart()。下面这段就是建单个标准图表控件的骨架,跑起来后子图能像主图一样横拉,还能用日历跳日期。 外汇和贵金属波动剧烈、杠杆高风险大,这类多图面板只是辅助观察,不代表任何方向判断。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建应用程序的类                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CProgram : class="kw">public CWndEvents
  {
class="kw">protected:
  class=class="str">"cmt">//--- 标准图表
  CStandardChart      m_sub_chart1;
  class=class="str">"cmt">//---
class="kw">protected:
  class=class="str">"cmt">//--- 标准图表
  class="type">bool                CreateSubChart1(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建标准图表 class="num">1                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateSubChart1(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap)
  {
class=class="str">"cmt">//--- 保存窗口指针
  m_sub_chart1.WindowPointer(m_window);
class=class="str">"cmt">//--- 加载第一个选卡
  m_tabs.AddToElementsArray(class="num">0,m_sub_chart1);
class=class="str">"cmt">//--- 坐标
  class="type">int x=m_window.X()+x_gap;
  class="type">int y=m_window.Y()+y_gap;
class=class="str">"cmt">//--- 创建之前设置属性
  m_sub_chart1.XSize(class="num">600);
  m_sub_chart1.YSize(class="num">200);
  m_sub_chart1.AutoXResizeMode(true);
  m_sub_chart1.AutoYResizeMode(true);
  m_sub_chart1.AutoXResizeRightOffset(class="num">175);
  m_sub_chart1.AutoYResizeBottomOffset(class="num">25);
  m_sub_chart1.XScrollMode(true);
class=class="str">"cmt">//--- 添加图表
  m_sub_chart1.AddSubChart("EURUSD",PERIOD_D1);
  m_sub_chart1.AddSubChart("EURGBP",PERIOD_D1);
  m_sub_chart1.AddSubChart("EURAUD",PERIOD_D1);
class=class="str">"cmt">//--- 创建控件
  if(!m_sub_chart1.CreateStandardChart(m_chart_id,m_subwin,x,y))
     class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 将对象添加到对象组的公共数组
  CWndContainer::AddToElementsArray(class="num">0,m_sub_chart1);
  class="kw">return(true);
  }

定时器空转吃掉一成 CPU 的排查与止血

早先在 Windows 7 x64 上跑的 Easy And Fast 库, 迁到 Windows 10 x64 后 CPU 占用明显抬升。图表上挂一个测试用 MQL 应用, 即便没有任何界面交互、纯待命状态, 库进程也能吃掉约 10% 的 CPU。 根因落在库引擎定时器上: 它每 16 ms 就刷新一次图表。下面这段代码里, OnTimerEvent 末尾无条件调用了 m_chart.Redraw(), 等于强迫图表持续重绘。 鼠标一旦在界面上移动、产生 CHARTEVENT_MOUSE_MOVE, ChartEventMouseMove 里又再 Redraw 一次, 双重重绘把消耗进一步推高。最直接的止血办法, 是从 ChartEventMouseMove 中删掉那行重绘调用 (原文中标红的一句)。 定时器本身不能全关——悬停变色、列表/表/日历的快转都依赖它。折中方案是: 鼠标动起来才激活定时器, 停手 500 ms 后阻塞其操作。具体在 CMouse 类里加一个调用计数器 m_call_counter 和 GapBetweenCalls() 方法, 后者返回两次鼠标移动事件之间的毫秒差。 CWndEvents 的定时器需加条件: 若鼠标超 500 ms 未动则不再重绘, 同时必须释放鼠标左键, 否则控件快转选项只能活 500 ms。实测去掉移动时的重绘对拖拽表单画质无影响, 因为 16 ms 的定时器间隔本来就够重绘。 这套改法只是最省事的其中一种, 库里还有别的降耗空间, 后面文章会继续拆。外汇与贵金属 MT5 环境高波动、高杠杆, 此类 GUI 优化只解决资源占用, 不波及任何交易信号概率。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 定时器                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::OnTimerEvent(class="type">void)
  {
class=class="str">"cmt">//--- 离开, 如果数组为空
   if(CWndContainer::WindowsTotal()<class="num">1)
      class="kw">return;
class=class="str">"cmt">//--- 定期检查所有元素事件
   CheckElementsEventsTimer();
class=class="str">"cmt">//--- 重绘图表 (红色高亮: 定时器里无条件重绘, 导致空转耗CPU)
   m_chart.Redraw();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CHARTEVENT 鼠标移动事件                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::ChartEventMouseMove(class="type">void)
  {
class=class="str">"cmt">//--- 离开, 如果这不是鼠标移动事件
   if(m_id!=CHARTEVENT_MOUSE_MOVE)
      class="kw">return;
class=class="str">"cmt">//--- 移动窗口
   MovingWindow();
class=class="str">"cmt">//--- 设置图表状态
   SetChartState();
class=class="str">"cmt">//--- 重绘图表 (标红: 鼠标移动时多余的重绘, 应删除以降CPU)
   m_chart.Redraw();
  }
class CMouse
  {
class="kw">private:
   class=class="str">"cmt">//--- 调用计数器
   class="type">class="kw">ulong            m_call_counter;
   class=class="str">"cmt">//---
class="kw">public: 
   class=class="str">"cmt">//--- 返回 (class="num">1) 最后调用期间保存的计数值, 及 (class="num">2) 调用鼠标移动事件处理器之间的差值
   class="type">class="kw">ulong            CallCounter(class="type">void)      class="kw">const { class="kw">return(m_call_counter);                }
   class="type">class="kw">ulong            GapBetweenCalls(class="type">void) class="kw">const { class="kw">return(::GetTickCount()-m_call_counter); }
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造器                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
CMouse::CMouse(class="type">void) : m_call_counter(::GetTickCount())
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 鼠标光标移动事件处理器                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMouse::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)
  {
class=class="str">"cmt">//--- 光标移动事件处理
   if(id==CHARTEVENT_MOUSE_MOVE)
     {

◍ 用定时器判定鼠标静止并释放左键

在 MT5 自定义控件里,鼠标事件和定时器事件是两套独立通道。上面的片段展示了如何在 OnTimerEvent 里用调用间隔判断光标是否静止:若两次调用差值超过 500 毫秒且左键已松开,就直接 return,不再处理后续逻辑。 这段判定依赖 m_mouse.GapBetweenCalls() 返回的毫秒数,它本质上是对 ::GetTickCount() 两次取值的差。代码里在鼠标事件入口处用 m_call_counter=::GetTickCount() 存了时间戳,定时器里再比间隔,属于典型的「事件+轮询」混合写法。 若窗口容器为空(WindowsTotal()<1)也直接退出,避免对不存在的控件做重绘。最后 CheckElementsEventsTimer() 扫一遍元素事件,再 m_chart.Redraw() 强制重绘——外汇与贵金属图表高频重绘有滑点外的渲染开销,实盘前建议在策略测试器里跑一遍看 CPU 占用。 别把 500ms 当默认值抄 这个阈值只是示例里的硬编码,不同品种 tick 频率差异大。黄金 M1 在美盘可能每秒数 tick,欧元兑美元亚盘清淡时几秒一跳;静止判定过严会丢拖拽,过松会卡重绘,建议按品种单独标定。

MQL5 / C++
  class=class="str">"cmt">//--- 鼠标左键坐标和状态
   m_x                = (class="type">int)lparam;
   m_y                = (class="type">int)dparam;
   m_left_button_state = (class="type">bool)class="type">int(sparam);
   class=class="str">"cmt">//--- 保存调用计数器值
   m_call_counter=::GetTickCount();
   class=class="str">"cmt">//--- 获取光标位置
   if(!::ChartXYToTimePrice(class="num">0,m_x,m_y,m_subwin,m_time,m_level))
      class="kw">return;
   class=class="str">"cmt">//--- 获取相对的 Y 坐标
   if(m_subwin>class="num">0)
      m_y=m_y-m_chart.SubwindowY(m_subwin);
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 定时器                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::OnTimerEvent(class="type">void)
  {
class=class="str">"cmt">//--- 离开, 如果鼠标光标处于静止状态 (调用之间的差值大于 500ms) 则释放鼠标左键。
  if(m_mouse.GapBetweenCalls()>class="num">500 && !m_mouse.LeftButtonState())
     class="kw">return;
class=class="str">"cmt">//--- 离开, 如果数组为空  
  if(CWndContainer::WindowsTotal()<class="num">1)
     class="kw">return;
class=class="str">"cmt">//--- 定期检查所有元素事件
  CheckElementsEventsTimer();
class=class="str">"cmt">//--- 重绘图表
  m_chart.Redraw();
  }

「给大数组预留内存能砍掉树形视图卡顿」

在 MT5 标准库中,CTreeView 初始化若承载大量节点会明显变慢,CFileNavigator 由于底层也用列表型数组,同样存在这个瓶颈。根因不在绘制本身,而在反复物理分配内存。 ArrayResize 的第三个参数就是为这个场景准备的:它指定预留尺寸,后续只有逻辑长度变化、物理内存不再重分配。频繁扩容时把预留值一次给足,能直接减少系统调用次数。 实测对比很直观:测试样本为超过 15 000 个文件的导航树,未设预留时初始化耗时显著;将数组预留大小定为 10 000 后,CTreeView 与 CFileNavigator 的响应都回到可用区间。外汇或贵金属 EA 面板若要做万级节点浏览,务必在 ArrayResize 调用里带上这个参数,否则高频品种切换可能卡死界面。

文件导航里的文件夹与文件图标替换

CFileNavigator 类现在支持在 MT5 文件导航器中挂载新的文件夹与文件图标,视觉逻辑仿照 Windows 10 资源管理器,比旧版默认图标更贴合开发库的图形界面风格。 若你的面板走自定义皮肤路线,也可以不走这套默认图标,自行传入定制图像即可,不强制绑定系统风。 原文给出的图例 6 展示了新图标在导航树里的实际排布效果,图像资源可在对应文章末尾打包获取,直接拷进 \experts\files 或界面资源目录就能被 CFileNavigator 引用。

◍ 别急着下结论

Easy And Fast 图形界面库眼下迭代到第四版(集成编译 4),压缩包体积 475.15 KB,已在文末开放下载。这一版的结构相较早期更完整,但作者明确在评论区回过:竖向图表布局不会在下一次更新落地,因为第二阶段开发里其他模块排期更靠前,具体时间给不出。 想拿它写 EA 面板的,建议先跑通现有横向布局的 CStandartChart 类,别等未发布的纵向支持。作者后续计划先接一套类 R plot 的可视化接口,再扩标准图表类——这意味着你的自定义控件若依赖布局方向,现在写死会踩坑。 外汇与贵金属行情高波动,拿这类 GUI 库做实盘前请在策略测试器里先验证稳定性,界面卡顿可能直接拖慢下单响应。

把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到多周期同窗的负载提示,你专注决策而不是调控件。

常见问题

核心加了水平滚动模式与鼠标光标切换,子图表内容可随光标横移,类似主图表操作,不再固定截断。
降低 CPU 负载,避免多控件同时刷新时主线程阻塞,实盘面板才不至于越开越卡。
在对应资源里补充黑白双色版本,导航树刷新时按状态切换,具体见本篇第 9 节实现。
不能直编 MQL,但多周期监视的呈现逻辑一致,小布的品种页已把类似视图和负载诊断打包好。
接口向后兼容,仅内部事件派发调整,老项目重新编译即可继承减负效果。