从新手到专家:使用 MQL5 制作动画新闻标题(一)·进阶篇
◍ 先把画布与事件分层搭起来
做 MT5 事件面板的第一步,是把用户输入、开发者常量和全局对象这三层先定清楚。输入参数里 InpTopOffset=50 控制距图表上边缘的像素偏移,ShowHigh/ShowMed/ShowLow 三个布尔值分别决定高、中、低影响事件泳道是否绘制,默认全是 true。 开发者常量固定了文字与色块之间的间距:时间标签“[1h]”后留 10px(GapTimeToSym),色块前留 5px(GapSymToRect),内联小方块边长 8px(RectSize),方块后到名称再留 10px(GapRectToName)。这些写死的值直接决定了一行事件在画布上的横向占位,调布局只改这里就够了。 全局区用 lineH=16 定义每行高度,canvW 存图表宽度,placeholder 是未接新闻源时的占位串。两个 CCanvas 实例 eventsCanvas 与 newsCanvas 分开承载事件与新闻层,避免重绘互相覆盖。 CEvent 类封装单条事件:time、sym、name、imp(影响等级),三个指针数组 highArr/medArr/lowArr 按影响度分流。每日重载守卫 lastReloadDay 初始为 0,防止同一天重复拉数据。 SetCanvas 负责把画布锚到左上或左下角,XDISTANCE 设 0 即贴左边。你直接把下面代码丢进 EA 的全局声明区,编译后能在 MT5 里看到空面板占位,证明分层没报错。
input class="type">int InpTopOffset = class="num">50; class=class="str">"cmt">// px offset from chart edge input class="type">bool ShowHigh = true; class=class="str">"cmt">// toggle High lane input class="type">bool ShowMed = true; class=class="str">"cmt">// toggle Medium lane input class="type">bool ShowLow = true; class=class="str">"cmt">// toggle Low lane class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| class="num">2) DEVELOPER CONSTANTS | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">static const class="type">int GapTimeToSym = class="num">10; class=class="str">"cmt">// px gap after “[1h]” class="kw">static const class="type">int GapSymToRect = class="num">5; class=class="str">"cmt">// px gap before class="kw">inline box class="kw">static const class="type">int RectSize = class="num">8; class=class="str">"cmt">// width & height of class="kw">inline box class="kw">static const class="type">int GapRectToName= class="num">10; class=class="str">"cmt">// px gap after class="kw">inline box class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| class="num">3) GLOBALS | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int lineH = class="num">16; class=class="str">"cmt">// row height in px class="type">int canvW; class=class="str">"cmt">// chart width class="type">class="kw">string placeholder = class=class="str">"cmt">// news ticker text "News feed coming soon – stay tuned with the calendar"; class="type">class="kw">datetime lastReloadDay = class="num">0; class=class="str">"cmt">// daily reload guard CCanvas eventsCanvas, newsCanvas; class=class="str">"cmt">// two layers class=class="str">"cmt">// Event class="kw">struct and arrays class CEvent : class="kw">public CObject { class="kw">public: class="type">class="kw">datetime time; class="type">class="kw">string sym, name; class="type">int imp; CEvent(class="type">class="kw">datetime t,const class="type">class="kw">string &S,const class="type">class="kw">string &N,class="type">int I) { time=t; sym=S; name=N; imp=I; } }; CEvent *highArr[], *medArr[], *lowArr[]; class="type">int offHigh, offMed, offLow, offNews; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Helper: position a canvas label | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void SetCanvas(class="type">class="kw">string name,class="type">bool top,class="type">int yDist) { ObjectSetInteger(class="num">0,name,OBJPROP_CORNER, top?CORNER_LEFT_UPPER:CORNER_LEFT_LOWER); ObjectSetInteger(class="num">0,name,OBJPROP_XDISTANCE, class="num">0);
按交易日刷新未来财经事件队列
事件指标要跑得稳,核心是每次只装载「今天零点到明天零点」这 86400 秒窗口内的未发生事件。ReloadEvents 先用 TimeTradeServer 取服务器时间,再拼出当日 00:00 的 datetime,若与上次刷新日相同就直接 return,避免每帧重算。 清理旧对象不能漏:highArr、medArr、lowArr 三个指针数组在 resize 归零前,必须逐个 delete 掉堆上的 CEvent,否则 MT5 跑几天就漏内存。 CalendarValueHistory 拉回的 vals 里混着已过去事件,用 vals[i].time <= srv 一句 continue 过滤掉,剩下按 CALENDAR_IMPORTANCE_HIGH / MODERATE 分流进不同数组。外汇与贵金属受这些事件冲击概率高,实测欧美盘前 3 小时 HIGH 级事件密集,加载逻辑若出错易在图形上叠满错乱标签。
class="type">void ReloadEvents() { class="type">class="kw">datetime srv = TimeTradeServer(); class=class="str">"cmt">// midnight today class="type">MqlDateTime dt; TimeToStruct(srv, dt); class="type">MqlDateTime m0 = {dt.year, dt.mon, dt.day,class="num">0,class="num">0,class="num">0}; class="type">class="kw">datetime today = StructToTime(m0); if(today == lastReloadDay) class="kw">return; lastReloadDay = today; class=class="str">"cmt">// clear previous for(class="type">int i=class="num">0;i<ArraySize(highArr);i++) class="kw">delete highArr[i]; for(class="type">int i=class="num">0;i<ArraySize(medArr); i++) class="kw">delete medArr[i]; for(class="type">int i=class="num">0;i<ArraySize(lowArr); i++) class="kw">delete lowArr[i]; ArrayResize(highArr,class="num">0); ArrayResize(medArr,class="num">0); ArrayResize(lowArr,class="num">0); class=class="str">"cmt">// fetch events [today, today+24h) MqlCalendarValue vals[]; class="type">int cnt = CalendarValueHistory(vals, today, today+class="num">86400); for(class="type">int i=class="num">0;i<cnt;i++) { if(vals[i].time <= srv) class="kw">continue; class=class="str">"cmt">// skip past MqlCalendarEvent e; if(!CalendarEventById(vals[i].event_id, e)) class="kw">continue; MqlCalendarCountry c; if(!CalendarCountryById(e.country_id, c)) class="kw">continue; class="type">class="kw">string sym = "[" + c.currency + "]"; CEvent *ev = new CEvent(vals[i].time, sym, e.name, e.importance); class=class="str">"cmt">// classify if(e.importance==CALENDAR_IMPORTANCE_HIGH) { class="type">int s=ArraySize(highArr)+class="num">1; ArrayResize(highArr,s); highArr[s-class="num">1]=ev; } else if(e.importance==CALENDAR_IMPORTANCE_MODERATE) { class="type">int s=ArraySize(medArr)+class="num">1; ArrayResize(medArr,s); medArr[s-class="num">1]=ev; } else
「事件栏的滚动绘制与重要性色块」
把财经事件排成一条横向滚动的信息带,核心在 DrawLane 这个函数:它按数组顺序逐个取出事件,先算距当前服务器时间还剩多少,再拼成「[1h]」或「[45m]」这样的倒计时标签。 字体固定用 Courier New、尺寸 -100,是为了等宽对齐,避免不同事件名长度不一导致整条带子抖动。每个事件依次画「倒计时 + 币种 + 色块 + 名称 + 分隔符」,其中色块用矩形填充:高影响红(255,0,0)、中影响橙(255,165,0)、低影响白(255,255,255)。 横向步进靠 TextWidth 动态算,比如倒计时标签画完 x 往后挪 TextWidth(part)-20,币种与矩形之间留 GapSymToRect 像素间隙。整条带子总宽 totalW = x - offset,随后 offset -= speed 实现左移滚动,到边界再 wrap 即可循环。 在 MT5 里跑这段,把 speed 设成 2~5 之间,能明显看到高影响事件(红色块)经过时视觉权重更突出,适合做贵金属盘面事件提醒,但外汇/贵金属受数据跳空影响大,实盘仍属高风险。
class="type">void DrawLane(CEvent* &arr[], class="type">int &offset, class="type">int y, class="type">int speed) { class="type">int n=ArraySize(arr); if(n==class="num">0) class="kw">return; class=class="str">"cmt">// monospaced for alignment eventsCanvas.FontNameSet("Courier New"); eventsCanvas.FontSizeSet(-class="num">100); class="type">int x = offset; class="type">class="kw">datetime srv = TimeTradeServer(); for(class="type">int i=class="num">0;i<n;i++) { CEvent *e = arr[i]; class=class="str">"cmt">// time-left “[1h]” or “[45m]” class="type">long diff = (class="type">long)e.time - (class="type">long)srv; class="type">class="kw">string tl = (diff>=class="num">3600 ? IntegerToString(diff/class="num">3600)+"h" : IntegerToString(diff/class="num">60)+"m"); class="type">class="kw">string part = "[" + tl + "]"; eventsCanvas.TextOut(x,y,part,XRGB(class="num">255,class="num">255,class="num">255),ALIGN_LEFT); x += eventsCanvas.TextWidth(part) -class="num">20; class=class="str">"cmt">// symbol “[USD]” eventsCanvas.TextOut(x,y,e.sym,XRGB(class="num">255,class="num">255,class="num">255),ALIGN_RIGHT); x += eventsCanvas.TextWidth(e.sym) + GapSymToRect; class=class="str">"cmt">// class="kw">inline importance box class="type">uint col = (e.imp==CALENDAR_IMPORTANCE_HIGH ? XRGB(class="num">255,class="num">0,class="num">0) : e.imp==CALENDAR_IMPORTANCE_MODERATE? XRGB(class="num">255,class="num">165,class="num">0): XRGB(class="num">255,class="num">255,class="num">255)); eventsCanvas.FillRectangle(x, y + (lineH-RectSize)/class="num">2, x+RectSize, y + (lineH-RectSize)/class="num">2 + RectSize, col); x += RectSize + GapRectToName; class=class="str">"cmt">// event name + separator eventsCanvas.TextOut(x,y,e.name,XRGB(class="num">255,class="num">255,class="num">255),ALIGN_RIGHT); x += eventsCanvas.TextWidth(e.name)+class="num">60; if(i+class="num">1<n) { eventsCanvas.TextOut(x,y,"|",XRGB(class="num">180,class="num">180,class="num">180),ALIGN_RIGHT); x += eventsCanvas.TextWidth("|") + class="num">20; } } class=class="str">"cmt">// scroll + wrap class="type">int totalW = x - offset; offset -= speed;
◍ 画布初始化与滚动刷新的衔接点
把三条事件泳道和新闻条塞进图表,靠的是 OnInit 里两次 CreateBitmapLabel:事件画布高度取 4*lineH,新闻画布只有 1*lineH,透明度分别设 150 与 0,前者半透、后者实心。canvW 直接用 ChartGetInteger(0,CHART_WIDTH_IN_PIXELS) 读当前图表像素宽,Offsets 全部从 canvW 起步,保证首次绘制时内容在右边界外待入。 DrawAll 每次先 Erase 事件画布(ARGB(180,0,0,0))再按 ShowHigh/Med/Low 逐个 DrawLane,y 坐标随 lineH 累加;新闻画布用 Tahoma、-120 字号写占位串,yOff 按行高与文本高度差除以 2 垂直居中。offNews 每帧减 InpNewsSpeed,越界(加文本宽小于 -20)就回卷到 canvW,形成无缝滚动。 OnInit 末尾 EventSetMillisecondTimer(InpTimerMs) 才真正驱动 OnTimer 周期重绘;lastReloadDay 清零强制当天重载,数组全 Resize(0) 清空。外汇与贵金属行情受事件扰动大、杠杆风险高,这类滚动提示仅作信息层参考,不代表任何方向判定。
if(offset + totalW < class="num">0) offset = canvW; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| DrawAll: render lanes + news row | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void DrawAll() { class=class="str">"cmt">// clear events eventsCanvas.Erase(ARGB(class="num">180,class="num">0,class="num">0,class="num">0)); class="type">int y=class="num">0; if(ShowHigh) { DrawLane(highArr, offHigh, y, InpSpeedHigh); y += lineH; } if(ShowMed) { DrawLane(medArr, offMed, y, InpSpeedMed); y += lineH; } if(ShowLow) { DrawLane(lowArr, offLow, y, InpSpeedLow); y += lineH; } eventsCanvas.Update(class="kw">false); class=class="str">"cmt">// news placeholder newsCanvas.Erase(ARGB(class="num">170,class="num">0,class="num">0,class="num">0)); newsCanvas.FontNameSet("Tahoma"); newsCanvas.FontSizeSet(-class="num">120); class="type">int yOff = (lineH - newsCanvas.TextHeight(placeholder)) / class="num">2; newsCanvas.TextOut(offNews, yOff, placeholder, XRGB(class="num">255,class="num">255,class="num">255), ALIGN_LEFT); offNews -= InpNewsSpeed; if(offNews + newsCanvas.TextWidth(placeholder) < -class="num">20) offNews = canvW; newsCanvas.Update(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| OnInit: setup canvases, initial load & position | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">// force reload Today lastReloadDay = class="num">0; class=class="str">"cmt">// clear arrays ArrayResize(highArr,class="num">0); ArrayResize(medArr,class="num">0); ArrayResize(lowArr,class="num">0); class=class="str">"cmt">// chart width canvW = (class="type">int)ChartGetInteger(class="num">0,CHART_WIDTH_IN_PIXELS); class=class="str">"cmt">// create events canvas(class="num">4 rows tall) eventsCanvas.CreateBitmapLabel("EvCanvas",class="num">0,class="num">0,canvW,class="num">4*lineH,COLOR_FORMAT_ARGB_RAW); eventsCanvas.TransparentLevelSet(class="num">150); class=class="str">"cmt">// create news canvas(class="num">1 row tall) newsCanvas.CreateBitmapLabel("NwCanvas",class="num">0,class="num">0,canvW,lineH,COLOR_FORMAT_ARGB_RAW); newsCanvas.TransparentLevelSet(class="num">0); class=class="str">"cmt">// load data + init offsets ReloadEvents(); offHigh = offMed = offLow = offNews = canvW; class=class="str">"cmt">// initial positioning { class="type">int rows = (ShowHigh?class="num">1:class="num">0)+(ShowMed?class="num">1:class="num">0)+(ShowLow?class="num">1:class="num">0); class="type">int yOff = InpTopOffset + (InpPositionTop ? class="num">0 : rows*lineH); SetCanvas("EvCanvas", InpPositionTop, InpTopOffset); SetCanvas("NwCanvas", InpPositionTop, yOff + (InpPositionTop ? rows*lineH : class="num">0)); } class=class="str">"cmt">// first draw & timer DrawAll(); EventSetMillisecondTimer(InpTimerMs); class="kw">return INIT_SUCCEEDED; }