从新手到专家:使用 MQL5 制作动画新闻标题(一)·进阶篇
📘

从新手到专家:使用 MQL5 制作动画新闻标题(一)·进阶篇

第 2/3 篇

◍ 先把画布与事件分层搭起来

做 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 里看到空面板占位,证明分层没报错。

MQL5 / C++
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 级事件密集,加载逻辑若出错易在图形上叠满错乱标签。

MQL5 / C++
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 之间,能明显看到高影响事件(红色块)经过时视觉权重更突出,适合做贵金属盘面事件提醒,但外汇/贵金属受数据跳空影响大,实盘仍属高风险。

MQL5 / C++
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) 清空。外汇与贵金属行情受事件扰动大、杠杆风险高,这类滚动提示仅作信息层参考,不代表任何方向判定。

MQL5 / C++
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;
}

常见问题

在画布初始化时按交易日时间戳比对,不匹配就清空事件队列并重新拉取未来财经事件,避免新旧混排。
给每条事件打重要性字段,绘制时用红/黄/灰色块区分,高影响占更宽栏位并置顶滚动。
小布可接入品种页自动汇总未来事件队列,按相关性高亮贵金属敏感数据,省去手动分层。
在初始化与滚动刷新间加衔接判断:仅当事件队列填充完毕且首帧绘制成功,才启动滚动定时器。
大概率是未做事件分层与按日刷新,全量常驻绘制拖慢帧率,按交易日裁剪并限滚动车道即可。