图形界面 IX: 进度条和线形图表控件 (第二章)·进阶篇
(2/3)· 从 CProgressBar 到 CLineGraph,手把手把两类高频 UI 控件接进你的 MQL5 程序
接上篇,我们继续深挖自定义图形界面库。长耗时任务里硬等终端没反馈,用户根本不知道脚本卡在哪一步;自己手画矩形模拟进度又难维护。本篇把进度条和线形图表两类控件做成可复用类,直接挂进现有 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 得出,外部可据此在标签上显示文本。外汇与贵金属图表加载此类控件存在刷新耗时,属高风险界面操作,实盘前应在策略测试器里验证重绘频率。
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 副图立刻看到变化。
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 回测属于高风险环境,滑点和个人参数误设都可能让界面表现和实盘脱节。
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,极端行情可能卡顿,属高风险操作环境。
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 可明显加快最慢那条进度条,外汇与贵金属品种波动剧烈,面板仅作辅助观察,实盘仍属高风险。
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 面板,接一笔余额数组,就能对比标准指标视图和线形视图的压缩差异。外汇与贵金属波动剧烈,图形仅作参考、不预示方向。