创建一个行情卷播面板:基本版·综合运用
(3/3)· 从资产文件读到面板绘制,把前两步的基础拼成能跑的基本版
「用 Edit 对象把多品种报价钉在图表上」
想在 MT5 图表上同时盯好几个品种,又不肯切窗口,可以用 C_Object_Edit 在画布上动态建文本框。下面这段逻辑先按配置逐个建对象:品种名框宽占 def_MaxWidth-1、高 22,价格框默认 0.0、宽 32,品种名框宽 10。 新建时不走恢复模式就会给 m_Infos.Symbols 扩容并写入代码,ArrayResize 的预留步长是 10,意味着每加一个品种最多多申请 10 个槽位,避免频繁重分配。nSymbols 自增后才算登记完成。 UpdateSymbolInfo 负责刷价:从 M1 取最近 1 根 K 线,CopyRates 只拉 1 根,把 close 写进价格框并染色(m_Infos.CorPrice),两个框的 X 轴都对到传入的 x 坐标,所以多品种会纵向对齐成一列。 配置从 Widget\ 目录下的 CSV 读,FILE_ANSI 编码;FileOpen 返回 INVALID_HANDLE 就直接 PrintFormat 报错并返 false,因此文件缺失时面板不会半残着跑。外汇与贵金属波动剧烈,这类浮动报价面板只作辅助观察,信号误读可能导致较大回撤。
edit.Size(A, def_MaxWidth - class="num">1, class="num">22); } C_Object_Edit edit; macro_Create(macro_ObjectName(def_CharSymbol, szArg), szArg, class="num">10); macro_Create(macro_ObjectName(def_CharPrice, szArg), class="num">0.0, class="num">32); if (!bRestore) { ArrayResize(m_Infos.Symbols, m_Infos.nSymbols + class="num">1, class="num">10); m_Infos.Symbols[m_Infos.nSymbols].szCode = szArg; m_Infos.nSymbols++; } class="macro">#undef macro_Create } class="kw">inline class="type">void UpdateSymbolInfo(class="kw">const class="type">int x, class="kw">const class="type">class="kw">string szArg) { C_Object_Edit edit; class="type">class="kw">string sz0 = macro_ObjectName(def_CharPrice, szArg); class="type">MqlRates Rate[class="num">1]; CopyRates(szArg, PERIOD_M1, class="num">0, class="num">1, Rate); edit.PositionAxleX(macro_ObjectName(def_CharSymbol, szArg), x); edit.SetTextValue(sz0, Rate[class="num">0].close, m_Infos.CorPrice); edit.PositionAxleX(sz0, x); } class="type">bool LoadConfig(class="kw">const class="type">class="kw">string szFileConfig) { class="type">int file; class="type">class="kw">string sz0; class="type">bool ret; if ((file = FileOpen("Widget\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { PrintFormat("Configuration file %s not found.", szFileConfig); class="kw">return class="kw">false;
配置加载与对象清理的底层写法
这段 C_Widget 析构与初始化代码,暴露了 MT5 面板类在生命周期上的两个关键动作:退出时回收图形资源、启动时读配置并建背景。 析构函数里 Terminal.Close() 先断图表终端对象,再用 ObjectsDeleteAll(Terminal.Get_ID(), def_PrefixName) 按前缀批量删对象,最后 ArrayFree(m_Infos.Symbols) 释放符号数组。若你做多品种看板,不写这一句,切换周期时可能残留几十个旧标签。 Initialize 接收配置文件路径与三种颜色参数,用 IndicatorSetString(INDICATOR_SHORTNAME, szNameShort) 设短名,Terminal.Init(ChartWindowFind()) 绑定子窗口,最后 return LoadConfig(szFileConfig) 把解析交给上文那段循环读取逻辑。 LoadConfig 中 ArrayResize(m_Infos.Symbols, 30, 30) 预留 30 个槽位,说明设计上限约 30 个品种;若配置文件行数超此数,需改这个常量,否则尾部品种会被静默丢弃。
} m_Infos.nSymbols = class="num">0; ArrayResize(m_Infos.Symbols, class="num">30, class="num">30); for (class="type">int c0 = class="num">1; (!FileIsEnding(file)) && (!_StopFlag); c0++) { if ((sz0 = FileReadString(file)) == "") class="kw">continue; if (SymbolExist(sz0, ret)) AddSymbolInfo(sz0); else { FileClose(file); PrintFormat("Ativo na linha %d não foi reconhecido.", c0); class="kw">return class="kw">false; } } FileClose(file); m_Infos.MaxPositionX = macro_MaxPosition; class="kw">return !_StopFlag; } ~C_Widget() { Terminal.Close(); ObjectsDeleteAll(Terminal.Get_ID(), def_PrefixName); ArrayFree(m_Infos.Symbols); } class="type">bool Initilize(class="kw">const class="type">class="kw">string szFileConfig, class="kw">const class="type">class="kw">string szNameShort, class="type">color corText, class="type">color corPrice, class="type">color corBack) { IndicatorSetString(INDICATOR_SHORTNAME, szNameShort); Terminal.Init(ChartWindowFind()); Terminal.Resize(); m_Infos.CorBackGround = corBack; m_Infos.CorPrice = corPrice; m_Infos.CorSymbol = corText; CreateBackGround(); class="kw">return LoadConfig(szFileConfig); } class="type">void DispatchMessage(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="kw">static class="type">int tx = class="num">0; class="type">class="kw">string szRet[]; class="kw">switch (id) { case (CHARTEVENT_CUSTOM + Ev_RollingTo):
◍ 面板拖拽与图表事件的分发逻辑
这段处理的是自定义监控面板在 MT5 图表上的交互响应。核心思路是把鼠标拖动、窗口缩放、对象删除三类事件分开处理,避免面板错位或残留。 tx 先叠加 lparam 的位移量,再用三目运算夹在 -def_MaxWidth 与 m_Infos.MaxPositionX 之间,实现左右越界回卷。随后按 m_Infos.nSymbols 个品种逐个右移 def_MaxWidth 像素调用 UpdateSymbolInfo,只刷新落在 Terminal.GetWidth() 内的部分,超出 MaxPositionX 就从左侧 -def_MaxWidth 续接。 CHARTEVENT_CHART_CHANGE 里只做两件事:Terminal.Resize() 重算可用区,把 m_Infos.MaxPositionX 重置为 macro_MaxPosition,再 ChartRedraw。窗口大小变了不重算坐标,面板就会永久偏出可视区。 CHARTEVENT_OBJECT_DELETE 要分辨删的是背景还是单个品种标签。前缀匹配 def_PrefixName 且按 '#' 拆出两段时,走 AddSymbolInfo 补回该品种;若删的是 def_NameObjBackGround,则清掉全部前缀对象并重建背景,再循环把所有已记录品种重新挂上。外汇与贵金属行情高波动,这类面板仅作辅助盯盘,信号滞后可能导致滑点风险。
tx = (class="type">int) (tx + lparam); tx = (tx < -def_MaxWidth ? m_Infos.MaxPositionX : (tx > m_Infos.MaxPositionX ? -def_MaxWidth : tx)); for (class="type">int c0 = class="num">0, px = tx; (c0 < m_Infos.nSymbols); c0++) { if (px < Terminal.GetWidth()) UpdateSymbolInfo(px, m_Infos.Symbols[c0].szCode); px += def_MaxWidth; px = (px > m_Infos.MaxPositionX ? -def_MaxWidth + (px - m_Infos.MaxPositionX) : px); } ChartRedraw(); break; case CHARTEVENT_CHART_CHANGE: Terminal.Resize(); m_Infos.MaxPositionX = macro_MaxPosition; ChartRedraw(); break; case CHARTEVENT_OBJECT_DELETE: if (StringSubstr(sparam, class="num">0, StringLen(def_PrefixName)) == def_PrefixName) if (StringSplit(sparam, &class="macro">#x27;#&class="macro">#x27;, szRet) == class="num">2) { AddSymbolInfo(szRet[class="num">1], true); ChartRedraw(); }else if (sparam == def_NameObjBackGround) { ObjectsDeleteAll(Terminal.Get_ID(), def_PrefixName); CreateBackGround(); for (class="type">int c0 = class="num">0; c0 < m_Infos.nSymbols; c0++) AddSymbolInfo(m_Infos.Symbols[c0].szCode, true);
「报价面板指标的定时器与绘制骨架」
这段代码片段展示了一个独立窗口指标的核心声明与事件驱动骨架:它不画常规曲线(indicator_plots 设为 0),而是用 45 像素高的子窗口承载报价面板。 关键参数里 user01 默认 -1 控制水平偏移,user02 默认 60 即定时器间隔毫秒数;OnInit 里 EventSetMillisecondTimer(user02) 决定了面板刷新节奏,调小该值可能让报价跟手但吃掉更多 CPU。 OnCalculate 直接返回 rates_total 不做计算,真正推图靠 OnTimer 发自定义事件 EventChartCustom,再由 Widget 对象接手重绘。外汇与贵金属报价跳动快,这种异步刷新结构在高波动时段可能比同步计算更稳。
class="macro">#class="kw">property copyright "Daniel Jose" class="macro">#class="kw">property description "Program for a panel of quotes." class="macro">#class="kw">property description "It creates a band that displays asset prices." class="macro">#class="kw">property description "For details on how to use it visit:\n" class="macro">#class="kw">property description "[MQL5官方文档] class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property indicator_separate_window class="macro">#class="kw">property indicator_plots class="num">0 class="macro">#class="kw">property indicator_height class="num">45 class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Widget\Rolling Price\C_Widget.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">input class="type">class="kw">string user00 = "Config.cfg"; class=class="str">"cmt">//Configuration file class="kw">input class="type">int user01 = -class="num">1; class=class="str">"cmt">//Shift class="kw">input class="type">int user02 = class="num">60; class=class="str">"cmt">//Pause in milliseconds class="kw">input class="type">color user03 = clrWhiteSmoke; class=class="str">"cmt">//Asset class="type">color class="kw">input class="type">color user04 = clrYellow; class=class="str">"cmt">//Price class="type">color class="kw">input class="type">color user05 = clrBlack; class=class="str">"cmt">//Background class="type">color class=class="str">"cmt">//+------------------------------------------------------------------+ C_Widget Widget; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { if (!Widget.Initilize(user00, "Widget Price", user03, user04, user05)) class="kw">return INIT_FAILED; EventSetMillisecondTimer(user02); class="kw">return INIT_SUCCEEDED; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[]) { class="kw">return rates_total; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTimer() { EventChartCustom(Terminal.Get_ID(), C_Widget::Ev_RollingTo, user01, class="num">0.0, ""); }
图表事件如何驱动面板横移
在 MT5 自定义面板里,鼠标拖拽或按钮触发的横向滚动,本质是靠 CHARTEVENT_CUSTOM 自定义事件把位移量送进 DispatchMessage。下面这段处理函数就是滚动逻辑的核心,改一处边界常量就能调出不同的回弹手感。 事件入口先由 OnChartEvent 把 id、lparam 等参数原样转交给 Widget.DispatchMessage,后者在收到 Ev_RollingTo 事件时,用静态变量 tx 累加 lparam 作为新的起始 X 坐标。 tx 被限制在 -def_MaxWidth 到 m_Infos.MaxPositionX 之间:小于左边界就跳到右端,大于右端就绕回左端,形成循环滚动。循环宽度 def_MaxWidth 若设成 120,而 m_Infos.MaxPositionX 为 800,则可视滚动区约 920 像素。 随后按 m_Infos.nSymbols 个品种依次右移 px 并调用 UpdateSymbolInfo 重绘,只要 px 还没超出 Terminal.GetWidth() 就更新对应报价块;越界部分折返到左侧继续排,最后 ChartRedraw 一次性刷新。外汇与贵金属报价面板的高频重绘会带来 CPU 占用,实盘前建议在策略测试器里跑一遍观察帧率。
class="type">void OnChartEvent(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) { Widget.DispatchMessage(id, lparam, dparam, sparam); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { EventKillTimer(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void DispatchMessage(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="kw">static class="type">int tx = class="num">0; class="type">class="kw">string szRet[]; class="kw">switch (id) { case (CHARTEVENT_CUSTOM + Ev_RollingTo): tx = (class="type">int) (tx + lparam); tx = (tx < -def_MaxWidth ? m_Infos.MaxPositionX : (tx > m_Infos.MaxPositionX ? -def_MaxWidth : tx)); for (class="type">int c0 = class="num">0, px = tx; (c0 < m_Infos.nSymbols); c0++) { if (px < Terminal.GetWidth()) UpdateSymbolInfo(px, m_Infos.Symbols[c0].szCode); px += def_MaxWidth; px = (px > m_Infos.MaxPositionX ? -def_MaxWidth + (px - m_Infos.MaxPositionX) : px); } ChartRedraw(); break;
◍ 自定义品种文件就能限定交易标的
行情卷播面板的基础版靠一个外部文件决定展示哪些资产,文件名随意,放在指定目录即可。文件里每行写一个品种名,必须和 MT5 市场观察窗口里的命名完全一致,改文件就行,不用动代码。 有个硬限制得记牢:同一个 MT5 实例里不能既看外汇货币对又看股票,这两类市场是隔离的,混开必然冲突。外汇和贵金属本身杠杆高、波动猛,真拿这套去跑实盘前,先想清楚自己是不是在赌隔离规则之外的意外。 附件里的示例文件是以 IBOV 指数成分股为模板的,想只看 EURUSD 或 XAUUSD,照着格式自建一个纯货币对清单丢进去,面板就只滚这些。下一步作者打算加资产图标,识别速度会更快,但当前这版先吃透文件机制最实在。