图形界面 IX: 进度条和线形图表控件 (第二章)·进阶篇
📊

图形界面 IX: 进度条和线形图表控件 (第二章)·进阶篇

(2/3)· 从 CProgressBar 到 CLineGraph,手把手把两类高频 UI 控件接进你的 MQL5 程序

实战向进阶 第 2/3 篇

接上篇,我们继续深挖自定义图形界面库。长耗时任务里硬等终端没反馈,用户根本不知道脚本卡在哪一步;自己手画矩形模拟进度又难维护。本篇把进度条和线形图表两类控件做成可复用类,直接挂进现有 MQL 应用。

进度条类的边界修正与重绘逻辑

在 MT5 自定义控件里封装进度条,核心不是画图,而是把调用方传来的脏数据先夹到合法区间。CProgressBar 私有成员 m_steps_total 与 m_current_index 分别管总步数和当前位置,两个 setter 都做了防越界:总步数小于 1 时强制为 1,当前索引小于 0 时归 1,大于总量时截断为总量。 StepsTotal 的修正代码很直白: void CProgressBar::StepsTotal(const int total) { m_steps_total=(total<1)? 1 : total; if(m_current_index>m_steps_total) m_current_index=m_steps_total; } 意思是外部若误传 0 或负数,内部仍按最少 1 步处理,避免除零。 CurrentIndex 同理夹住下界和上界,保证 m_current_index 永远落在 [1, m_steps_total]。 void CProgressBar::CurrentIndex(const int index) { if(index<0) m_current_index=1; else m_current_index=(index>m_steps_total)? m_steps_total : index; } Update 一次性接收 index 和 total,先调上面两个私有方法,再算指示条宽度:new_width=(m_current_index/m_steps_total)*m_bar_bg.XSize()。若算出来不足 1 像素则取 1,否则扣掉左右边框各 m_bar_border_width 后做上限截断。 最后百分比由 percent=m_current_index/m_steps_total*100 得出,外部可据此在标签上显示文本。外汇与贵金属图表加载此类控件存在刷新耗时,属高风险界面操作,实盘前应在策略测试器里验证重绘频率。

MQL5 / C++
class="type">void CProgressBar::StepsTotal(class="kw">const class="type">int total)
  {
  m_steps_total=(total<class="num">1)? class="num">1 : total;
  if(m_current_index>m_steps_total)
     m_current_index=m_steps_total;
  }
class="type">void CProgressBar::CurrentIndex(class="kw">const class="type">int index)
  {
  if(index<class="num">0)
     m_current_index=class="num">1;
  else
     m_current_index=(index>m_steps_total)? m_steps_total : index;
  }
class="type">void CProgressBar::Update(class="kw">const class="type">int index,class="kw">const class="type">int total)
  {
  CurrentIndex(index);
  StepsTotal(total);
  class="type">class="kw">double new_width=(m_current_index/m_steps_total)*m_bar_bg.XSize();
  if((class="type">int)new_width<class="num">1)
     new_width=class="num">1;
  else
     {
     class="type">int x_size=m_bar_bg.XSize()-(m_bar_border_width*class="num">2);
     if((class="type">int)new_width>=x_size)
        new_width=x_size;
     }
  m_indicator.X_Size((class="type">int)new_width);
  class="type">class="kw">double percent =m_current_index/m_steps_total*class="num">100;

◍ 把超限百分比钳进标签显示

做自定义指标面板时,进度类标签若直接把计算出的 percent 丢进 Description,遇到统计窗口放大或样本缺失会导致数值越过 100% 显得不专业。上面这段用三元运算符把 percent 上限锁死在 100,再按 m_digits 指定位数转成字符串拼上百分号。 DoubleToString 的第二个参数 m_digits 通常由外部输入参数控制,常见设 1 或 2;若 m_digits=1 且真实 percent=123.4,最终 desc 只会显示 "100.0%" 而非越界值。 调参时留意:m_percent 这个 CCanvas 或自定义图形对象的 Description 只负责静态文本,不会自动重绘,改完记得触发 ChartRedraw() 才能在 MT5 副图立刻看到变化。

MQL5 / C++
  class="type">class="kw">string desc = DoubleToString((percent>class="num">100)? class="num">100 : percent,m_digits)+"%";
class=class="str">"cmt">//--- 设置新的值
  m_percent.Description(desc);
  }

「用滑轨驱动八个进度条」

拿之前带主菜单和状态条的 EA 模板改最简单:删掉其余控件,只留框架,往界面上塞 8 个 CProgressBar,再配一个 CSlider 滑轨用来实时调迭代次数,比写死循环有意思也更能测边界。 CProgram 类里先声明 1 个滑轨和 8 个进度条实例,每个进度条用 GAP_X / GAP_Y 宏定死距表单左边的像素:X 统一 7,Y 从 100 起每 25 像素排一个,到第 8 个落到 275。这样纵向不重叠,肉眼能直接比对各条推进速度。 模拟逻辑放在 OnTimerEvent() 里:每次计时器触发先读滑轨当前值作为步长,8 个静态计数器各自加不同增量,进度条就呈现异步走动。编译挂图后能看到条子快慢不一,说明控件刷新没卡 UI 线程。外汇和贵金属图表上跑这类 GUI 回测属于高风险环境,滑点和个人参数误设都可能让界面表现和实盘脱节。

MQL5 / C++
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
   class=class="str">"cmt">//--- 滑轨
   CSlider            m_slider1;
   class=class="str">"cmt">//--- 进度条
   CProgressBar       m_progress_bar1;
   CProgressBar       m_progress_bar2;
   CProgressBar       m_progress_bar3;
   CProgressBar       m_progress_bar4;
   CProgressBar       m_progress_bar5;
   CProgressBar       m_progress_bar6;
   CProgressBar       m_progress_bar7;
   CProgressBar       m_progress_bar8;
   class=class="str">"cmt">//---
class="kw">private:
   class=class="str">"cmt">//--- 滑轨
class="macro">#define SLIDER1_GAP_X(class="num">7)
class="macro">#define SLIDER1_GAP_Y(class="num">50)
   class="type">bool                CreateSlider1(class="kw">const class="type">class="kw">string text);
class=class="str">"cmt">//---
class="macro">#define PROGRESSBAR1_GAP_X(class="num">7)
class="macro">#define PROGRESSBAR1_GAP_Y(class="num">100)
   class="type">bool                CreateProgressBar1(class="type">void);
class=class="str">"cmt">//---
class="macro">#define PROGRESSBAR2_GAP_X(class="num">7)
class="macro">#define PROGRESSBAR2_GAP_Y(class="num">125)
   class="type">bool                CreateProgressBar2(class="type">void);
class=class="str">"cmt">//---
class="macro">#define PROGRESSBAR3_GAP_X(class="num">7)
class="macro">#define PROGRESSBAR3_GAP_Y(class="num">150)
   class="type">bool                CreateProgressBar3(class="type">void);
class=class="str">"cmt">//---
class="macro">#define PROGRESSBAR4_GAP_X(class="num">7)
class="macro">#define PROGRESSBAR4_GAP_Y(class="num">175)
   class="type">bool                CreateProgressBar4(class="type">void);
class=class="str">"cmt">//---
class="macro">#define PROGRESSBAR5_GAP_X(class="num">7)
class="macro">#define PROGRESSBAR5_GAP_Y(class="num">200)
   class="type">bool                CreateProgressBar5(class="type">void);
class=class="str">"cmt">//---
class="macro">#define PROGRESSBAR6_GAP_X(class="num">7)
class="macro">#define PROGRESSBAR6_GAP_Y(class="num">225)
   class="type">bool                CreateProgressBar6(class="type">void);
class=class="str">"cmt">//---
class="macro">#define PROGRESSBAR7_GAP_X(class="num">7)
class="macro">#define PROGRESSBAR7_GAP_Y(class="num">250)
   class="type">bool                CreateProgressBar7(class="type">void);
class=class="str">"cmt">//---
class="macro">#define PROGRESSBAR8_GAP_X(class="num">7)
class="macro">#define PROGRESSBAR8_GAP_Y(class="num">275)
   class="type">bool                CreateProgressBar8(class="type">void);
   };

进度条与面板的实例化细节

在 MT5 自定义面板里,进度条不是凭空画的,得先绑父窗口再设几何参数。下面这段 CreateProgressBar1 把进度条 1 挂到 m_window1 下,X 偏移用 PROGRESSBAR1_GAP_X、Y 偏移用 PROGRESSBAR1_GAP_Y 相对表单定位,避免写死坐标。 控件本体宽 220、高 15,内部填充条宽 123、高 11,填充条相对控件左上角偏移 (65,2),左侧留白放“Progress 01:”标签。这些数直接决定视觉占比,想塞进窄面板就把 XSize 从 220 往下压。 CreateExpertPanel 负责把滑轨和 8 个进度条按顺序拼起来:先 CreateSlider1 拿到“迭代总次数”,再连续调 CreateProgressBar1 到 8,任一个返回 false 就整体中断。最后 m_chart.Redraw() 触发一次重绘,不然控件建完不显示。 OnTimerEvent 里才是动态核心:total 取自滑轨值,8 个静态计数器各自按不同步长累加(如 count1 每次 +8、count2 每次 +3),到 total 归零循环。这模拟了多任务异步推进的观感,你可以把 +8 改成 +1 看条子走得更细腻。外汇与贵金属图表加载此类面板需注意:高频 Redraw 会吃 CPU,极端行情可能卡顿,属高风险操作环境。

MQL5 / C++
class="type">bool CProgram::CreateProgressBar1(class="type">void)
  {
class=class="str">"cmt">//--- 保存表单的指针
   m_progress_bar1.WindowPointer(m_window1);
class=class="str">"cmt">//--- 坐标
   class="type">int x=m_window1.X()+PROGRESSBAR1_GAP_X;
   class="type">int y=m_window1.Y()+PROGRESSBAR1_GAP_Y;
class=class="str">"cmt">//--- 在创建之前设置属性
   m_progress_bar1.XSize(class="num">220);
   m_progress_bar1.YSize(class="num">15);
   m_progress_bar1.BarXSize(class="num">123);
   m_progress_bar1.BarYSize(class="num">11);
   m_progress_bar1.BarXOffset(class="num">65);
   m_progress_bar1.BarYOffset(class="num">2);
   m_progress_bar1.LabelText("Progress class="num">01:");
class=class="str">"cmt">//--- 创建控件
   if(!m_progress_bar1.CreateProgressBar(m_chart_id,m_subwin,x,y))
      class="kw">return(false);
class=class="str">"cmt">//--- 把控件指针加到库中
   CWndContainer::AddToElementsArray(class="num">0,m_progress_bar1);
   class="kw">return(true);
  }

class="type">bool CProgram::CreateExpertPanel(class="type">void)
  {
   if(!CreateSlider1("迭代总次数:"))
      class="kw">return(false);
   if(!CreateProgressBar1())
      class="kw">return(false);
   if(!CreateProgressBar2())
      class="kw">return(false);
   if(!CreateProgressBar3())
      class="kw">return(false);
   if(!CreateProgressBar4())
      class="kw">return(false);
   if(!CreateProgressBar5())
      class="kw">return(false);
   if(!CreateProgressBar6())
      class="kw">return(false);
   if(!CreateProgressBar7())
      class="kw">return(false);
   if(!CreateProgressBar8())
      class="kw">return(false);
   m_chart.Redraw();
   class="kw">return(true);
  }

class="type">void CProgram::OnTimerEvent(class="type">void)
  {
   CWndEvents::OnTimerEvent();
   class="type">int total=(class="type">int)m_slider1.GetValue();
   class="kw">static class="type">int count1=class="num">0;
   count1=(count1>=total) ? class="num">0 : count1+=class="num">8;
   m_progress_bar1.Update(count1,total);
   class="kw">static class="type">int count2=class="num">0;
   count2=(count2>=total) ? class="num">0 : count2+=class="num">3;
   m_progress_bar2.Update(count2,total);
   class="kw">static class="type">int count3=class="num">0;

◍ 多进度条错频刷新的实现细节

这段逻辑在 OnTimer 里同时驱动 7 条进度条和 1 个状态条,每条用独立的 static int 计数,增量各不相同:count3 每次 +12、count4 +6、count5 +18、count6 +10、count7 +1、count8 +15。 当计数值大于等于 total 时归零重跑,因此不同进度条会以不同速度循环,视觉上形成错频效果,适合在面板里区分多个指标的刷新节奏。 状态条走另一条路:count9 累计 TIMER_STEP_MSC,未到 TIMER_STEP_MSC*10 就直接 return,到达后才把本地时间写入状态条第 1 项并清零。 开 MT5 把这段贴进 EA 的 OnTimer,调大 count7 的 +1 为 +5 可明显加快最慢那条进度条,外汇与贵金属品种波动剧烈,面板仅作辅助观察,实盘仍属高风险。

MQL5 / C++
   count3=(count3>=total) ? class="num">0 : count3+=class="num">12;
   m_progress_bar3.Update(count3,total);
class=class="str">"cmt">//---
   class="kw">static class="type">int count4=class="num">0;
   count4=(count4>=total) ? class="num">0 : count4+=class="num">6;
   m_progress_bar4.Update(count4,total);
class=class="str">"cmt">//---
   class="kw">static class="type">int count5=class="num">0;
   count5=(count5>=total) ? class="num">0 : count5+=class="num">18;
   m_progress_bar5.Update(count5,total);
class=class="str">"cmt">//---
   class="kw">static class="type">int count6=class="num">0;
   count6=(count6>=total) ? class="num">0 : count6+=class="num">10;
   m_progress_bar6.Update(count6,total);
class=class="str">"cmt">//---
   class="kw">static class="type">int count7=class="num">0;
   count7=(count7>=total) ? class="num">0 : count7+=class="num">1;
   m_progress_bar7.Update(count7,total);
class=class="str">"cmt">//---
   class="kw">static class="type">int count8=class="num">0;
   count8=(count8>=total) ? class="num">0 : count8+=class="num">15;
   m_progress_bar8.Update(count8,total);

class=class="str">"cmt">//--- 状态条的计时器函数
   class="kw">static class="type">int count9=class="num">0;
   if(count9<TIMER_STEP_MSC*class="num">10)
     {
      count9+=TIMER_STEP_MSC;
      class="kw">return;
     }
   count9=class="num">0;
   m_status_bar.ValueToItem(class="num">1,::TimeToString(::TimeLocal(),TIME_DATE|TIME_SECONDS));
   }

「让任意长度数组都能挤进线形图」

MetaTrader 终端自带的指标标准类型程序在显示数据数组时并不顺手:窗口尺度设置只能从 0(最强压缩)到 5,最强压缩下 1 个像素代表 1 条记录,数组放不下就得靠滚动条;想多看数据只能切更高时段,但烛形或柱形下也只是在指定时段里看个大概范围。 真正好用的线形图应该像 Excel 那样——不限制缩放,任何大小的数组都能塞进宽度大于 1 像素的区域,且第一个点贴左、最后一个点贴右,最小值和最大值都不丢。实测 Excel 线形图可容纳 50000 个数据的数组而无损失。 标准库已提供图表类,路径在 <data folder>\MQLX\Include\Canvas\Charts:CChartCanvas 是基类,CLineChart 派生做线形图,CHistogramChart 做柱形图,CPieChart 做饼图。当前这些类算临时方案,部分方法后续会改或替换,未来会另写库提升绘制质量。 开 MT5 进 Include\Canvas\Charts 把 CLineChart 拖进 EA 面板,接一笔余额数组,就能对比标准指标视图和线形视图的压缩差异。外汇与贵金属波动剧烈,图形仅作参考、不预示方向。

把控件状态交给小布盯盘看
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到控件渲染与事件绑定的异常提示,你专注把交互逻辑写顺。

常见问题

这两个方法只负责把外部传入的步数和索引限制在有效范围内,避免越界导致进度条绘制错乱,对外暴露主交互方法即可,内部修正逻辑不必开放。
可以,小布盯盘内置的 AIGC 诊断能标记控件事件绑定缺失或坐标偏移异常,省去你逐帧排查界面卡顿的重复劳动。
自带图表偏行情序列,自定义线形图表能按你的指标逻辑在面板内画过程曲线,比如回测收益走线,不占用主图也不受品种限制。
在 CProgressBar 类初始化时通过配置项指定小数点位数,默认通常不留小数,按展示精度需求自行传入参数即可。
关于库结构完整讨论见《图形界面 IX: 库结构的准备工作·基础篇》。