创建一个行情卷播面板:改进版·进阶篇
「C_Widget 类里那些不显眼却要命的改动」
打开 C_Widget.mqh 对照第一版基本系统,最直观的变化是挂上了 C_Object_BtnBitMap.mqh,单元格宽度也调宽了,能塞进更多明细。但真正容易坑人的是那个生成对象名的宏替换——它在类里会被反复调用,名字唯一性全靠它。 私密声明区看着没动,其实枚举 EventCustom 和 EnumTypeObject 的玩法变了。EnumTypeObject 里对象类型枚举从 35 起跳,不代表创建什么对象,而是指示用途;用枚举而非 #define,是为了杜绝两个宏撞出同一个值导致对象名不唯一的隐患。 为什么是 35 而不是更小?看 macro_ObjectName 宏:第一个参数是枚举值,经 CharToString 转成字符拼进对象名。ASCII 表里 32 是空格,是最小可用字符,所以枚举底子从 32 起,35 只是顺推。若改成数值转字符串就能从 0 起,但字符映射下最小适配就是 32。 背景创建函数里顺手处理了 OnChartEvent 的图表 resize:面板高度固定,宽度用 macro_MaxPosition 保底撑大,背景永远够用。添加对象时按 BitMap→资产代码→日百分比→方向箭头→当前价 的顺序建,且每个名字都带唯一字符。位图资源必须是 32x32 像素,别的尺寸得在别处调,但所有对象尺寸基准都在这函数定,想改面板大小就动这里。 数据呈现段强制序列从新到旧,抓日线收盘价算日变百分比;若返回条数不足预期直接退函数,避免指标在 MT5 工具箱刷错误。画布定位先放图,宽度值引自 def_MaxWidth(代码里设的 160),若面板加宽不调这个编译定义,信息会怪异地折行,设太高又拖慢重绘。 箭头用 Wingdings 3 字体画涨跌方向,换字体就得改对应字符值。Edit 对象用重载函数吞字符串和双精度,内部靠 '%' 符号区分百分比。最后价格颜色在函数中直改,系统对外调用方式没变,指标侧只动了高度值设定。
class="macro">#include "Elements\C_Object_Edit.mqh" class="macro">#include "Elements\C_Object_BackGround.mqh" class="macro">#include "Elements\C_Object_BtnBitMap.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ C_Terminal Terminal; class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_PrefixName "WidgetPrice" class="macro">#define def_MaxWidth class="num">160 class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define macro_MaxPosition(Terminal.GetWidth() >= (m_Infos.nSymbols * def_MaxWidth) ? Terminal.GetWidth() : m_Infos.nSymbols * def_MaxWidth) class="macro">#define macro_ObjectName(A, B) (def_PrefixName + (class="type">class="kw">string)Terminal.GetSubWin() + CharToString(A) + "#" + B) class C_Widget { class="kw">protected: enum EventCustom {Ev_RollingTo}; class="kw">private : enum EnumTypeObject {en_Background = class="num">35, en_Symbol, en_Price, en_Percentual, en_Icon, en_Arrow}; class="kw">struct st00 { class="type">color CorBackGround, CorSymbol; class="type">int nSymbols, MaxPositionX; class="kw">struct st01
◍ 用 GUI 对象搭一个自选品种面板
在 MT5 里做自定义盯盘面板,核心是把每个品种的信息拆成可定位的 GUI 对象。下面这段代码演示了背景与品种条目的创建逻辑,开 MT5 新建 EA 把结构抄进去就能看到雏形。 背景对象先按终端屏幕宽和高铺满:CreateBackGround 里用 TerminalInfoInteger(TERMINAL_SCREEN_WIDTH) 取屏宽,Terminal.GetHeight() 取面板高,颜色走 m_Infos.CorBackGround。对象名由宏 macro_ObjectName 拼出来,格式是 前缀+子窗口号+类型字符+#+参数,保证同图表多实例不重名。 AddSymbolInfo 给单个品种挂四块东西:图标 bmp(X 轴先置 9999 离屏,Y=15)、品种名 edit(Y=10,尺寸 56×16)、百分比 edit(Lucida Console 8号字,尺寸 50×11)、箭头 edit(Wingdings 3 字体 10号)。所有 X 统一设 9999,说明真实排布靠后续重定位,这里只是先把对象建出来。 外汇和贵金属波动大,这类面板只解决信息密度问题,不替代风控;实盘前先在策略测试器用历史数据验证对象刷新会不会卡顿。
{
class="type">class="kw">string szCode;
}Symbols[];
}m_Infos;
class=class="str">"cmt">// ... Rest of the code
class="type">void CreateBackGround(class="type">void)
{
C_Object_BackGround backGround;
class="type">class="kw">string sz0 = macro_ObjectName(en_Background, "");
backGround.Create(sz0, m_Infos.CorBackGround);
backGround.Size(sz0, TerminalInfoInteger(TERMINAL_SCREEN_WIDTH), Terminal.GetHeight());
}
class="macro">#define macro_ObjectName(A, B) (def_PrefixName + (class="type">class="kw">string)Terminal.GetSubWin() + CharToString(A) + "#" + B)
class="type">void AddSymbolInfo(class="kw">const class="type">class="kw">string szArg, class="kw">const class="type">bool bRestore = false)
{
C_Object_Edit edit;
C_Object_BtnBitMap bmp;
class="type">class="kw">string sz0;
class="kw">const class="type">int x = class="num">9999;
bmp.Create(sz0 = macro_ObjectName(en_Icon, szArg), szArg);
bmp.PositionAxleX(sz0, x);
bmp.PositionAxleY(sz0, class="num">15);
edit.Create(sz0 = macro_ObjectName(en_Symbol, szArg), m_Infos.CorSymbol, m_Infos.CorBackGround, szArg);
edit.PositionAxleX(sz0, x);
edit.PositionAxleY(sz0, class="num">10);
edit.Size(sz0, class="num">56, class="num">16);
edit.Create(sz0 = macro_ObjectName(en_Percentual, szArg), m_Infos.CorSymbol, m_Infos.CorBackGround, class="num">0.0, "Lucida Console", class="num">8);
edit.PositionAxleX(sz0, x);
edit.PositionAxleY(sz0, class="num">10);
edit.Size(sz0, class="num">50, class="num">11);
edit.Create(sz0 = macro_ObjectName(en_Arrow, szArg), m_Infos.CorSymbol, m_Infos.CorBackGround, "", "Wingdings class="num">3", class="num">10);在图表上堆出多品种日线涨跌面板
这段逻辑干两件事:先给每个品种画一个可编辑文本控件显示代码,再在 UpdateSymbolInfo 里算日线两日收盘变化率并摆好图标与箭头。控件坐标用 PositionAxleX / Y 定锚,Size 里宽高直接写死,比如 20×16 和 60×16,肉眼可见的紧凑排布。 核心数据提取在 CopyClose(szArg, PERIOD_D1, 0, 2, v0):取日线最新两根收盘,ArraySetAsSeries 设成时间倒序,v0[0] 是今收、v0[1] 是昨收。变化率 v1 = (v0[0]-v0[1])/max(v0[0],v0[1])*100.0,涨用代码 230 的字符、跌用 232,颜色走正绿负红宏。 非恢复模式下用 ArrayResize(m_Infos.Symbols, n+1, 10) 增量扩数组,预留 10 单元避免频繁重分配;x 坐标随图标 XSIZE 累加,所以多品种是从左往右流式排开。外汇和贵金属波动受杠杆与消息驱动,这类面板只做参考,实际下单仍以小周期结构为准。
edit.PositionAxleX(sz0, x); edit.PositionAxleY(sz0, class="num">26); edit.Size(sz0, class="num">20, class="num">16); edit.Create(sz0 = macro_ObjectName(en_Price, szArg), class="num">0, m_Infos.CorBackGround, class="num">0.0); edit.PositionAxleX(sz0, x); edit.PositionAxleY(sz0, class="num">26); edit.Size(sz0, class="num">60, class="num">16); 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="kw">inline class="type">void UpdateSymbolInfo(class="type">int x, class="kw">const class="type">class="kw">string szArg) { C_Object_Edit edit; C_Object_BtnBitMap bmp; class="type">class="kw">string sz0; class="type">class="kw">double v0[], v1; ArraySetAsSeries(v0, true); if (CopyClose(szArg, PERIOD_D1, class="num">0, class="num">2, v0) < class="num">2) class="kw">return; v1 = ((v0[class="num">0] - v0[class="num">1]) / v0[(v0[class="num">0] > v0[class="num">1] ? class="num">0 : class="num">1)]) * class="num">100.0; bmp.PositionAxleX(sz0 = macro_ObjectName(en_Icon, szArg), x); x += (class="type">int) ObjectGetInteger(Terminal.Get_ID(), sz0, OBJPROP_XSIZE); edit.PositionAxleX(macro_ObjectName(en_Symbol, szArg), x + class="num">2); edit.PositionAxleX(sz0 = macro_ObjectName(en_Arrow, szArg), x + class="num">2); edit.SetTextValue(sz0, CharToString(v1 >= class="num">0 ? (class="type">uchar)class="num">230 : (class="type">uchar)class="num">232), (v1 >= class="num">0 ? def_ColoPositive : def_ColorNegative));
「滚动报价面板的绘制与初始化」
这段代码片段来自一个独立子窗口的指标,专门在图表上做轻量级的滚动价格显示,不画任何常规 plot。
#property indicator_separate_window 与 indicator_plots 0 说明它独占一个 32 像素高的副窗(indicator_height 32),主图区域完全不被占用,适合盯盘时常驻一角。
核心对象 C_Widget Widget 通过 Widget.Initilize(user00, "Widget Price", user03, user04) 载入配置文件与配色;若初始化失败直接返回 INIT_FAILED,避免空跑。
定时器由 EventSetMillisecondTimer(user02) 驱动,默认 user02 = 10 毫秒刷新一次,配合 user01 = -1 的偏移量控制显示位置。外汇与贵金属波动快,这种高频刷新面板能降低肉眼读价延迟,但高频重绘也会吃 CPU,实盘前应在 MT5 用任务管理器看一眼占用。
文本写入逻辑里,edit.SetTextValue 先写百分比(v1 加 % 后缀),X 轴偏移 x + 62;再写价格(v0[0] 按 v1 正负决定乘 1 或 -1),X 轴偏移 x + 24。复制下面代码到 MT5 的 C_Widget 派生指标里即可验证布局。
edit.SetTextValue(sz0 = macro_ObjectName(en_Percentual, szArg), v1 , clrNONE, "%"); edit.PositionAxleX(sz0, x + class="num">62); edit.SetTextValue(sz0 = macro_ObjectName(en_Price, szArg), v0[class="num">0] * (v1 >= class="num">0 ? class="num">1 : -class="num">1)); edit.PositionAxleX(sz0, x + class="num">24); } 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">32 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">//Settings 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">10; 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 = clrBlack; class=class="str">"cmt">//Price 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)) class="kw">return INIT_FAILED; EventSetMillisecondTimer(user02); class="kw">return INIT_SUCCEEDED; }
◍ 点一下面板就跳对应图表
做报价面板时,最实用的交互不是显示价格,而是点一下标的名就能直接开图。MetaTrader 5 里不用敲键盘换品种,只要在对象创建时嵌入特定格式(比如用 # 分隔面板名和品种名),点击事件里就能反查出你点的是哪个资产。 核心靠 StringSplit 按 '#' 拆字符串。若拆分返回 2 段,说明格式对,第二段就是品种。先用 SymbolSelect 把它塞进市场报价窗口,再抓当前图表的品种代码,尝试切过去;切失败了就把刚才加进报价的品种删掉,避免污染观察列表。外汇和贵金属波动大、杠杆高,误加一堆品种只会拖慢面板重绘。 两个落地细节:新图表默认沿用旧图时间帧,不会自动跳到你习惯的周期;每次换资产或周期,面板都从列表第一个重画,所以品种数控制在十几个内比较合理,多了刷新明显变慢。下面这段消息处理片段就是点击跳转的关键。
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) { class=class="str">"cmt">// ... Internal code... case CHARTEVENT_OBJECT_CLICK: if (StringSplit(sparam, &class="macro">#x27;#&class="macro">#x27;, szRet) == class="num">2) { SymbolSelect(szRet[class="num">1], true); szRet[class="num">0] = ChartSymbol(Terminal.Get_ID());
把自选列表落进配置文件
这段收尾逻辑干一件事:把当前 Market Watch 里的品种名批量写进 Widget\Config.cfg,供前面的切换函数读取。它只遍历 SymbolsTotal(inWatch) 返回的数量,inWatch 设为 true 时只扫自选窗口,设 false 才会覆盖终端全部品种。
| 核心函数 ativos() 用 FileOpen 以 FILE_WRITE | FILE_ANSI | FILE_CSV 模式打开文件,分隔符是逗号。若句柄等于 INVALID_HANDLE 就弹窗报错并 return,否则用 for 循环把 SymbolName(i,inWatch) 逐个 FileWrite,最后 FileClose 并提示 File created。 |
|---|
注释里留了 ISIN、PAGE、PATH 的写法,但当前版本只落了纯符号名。你在 MT5 里跑完这个函数,去终端数据文件夹的 Widget 目录就能看到 Config.cfg,用记事本打开应是每行一个品种,例如 EURUSD、XAUUSD。外汇与贵金属波动剧烈,脚本只解决符号管理,不构成任何方向建议。
class="type">void ativos() { class=class="str">"cmt">//--- class="kw">const class="type">bool inWatch=true; class=class="str">"cmt">// True - 仅 MarketWatch 中的符号 class="kw">const class="type">bool allSymbols=false; class=class="str">"cmt">// 假 - 所有符号 class="type">int handleFile=FileOpen("Widget\\Config.cfg",FILE_WRITE|FILE_ANSI|FILE_CSV,","); if(handleFile==INVALID_HANDLE) { Alert("Error opening file"); class="kw">return; } class=class="str">"cmt">// FileWrite(handleFile, "Symbol", "ISIN"); for(class="type">int i=class="num">0;i<SymbolsTotal(inWatch);i++) { FileWrite(handleFile,SymbolName(i,inWatch)); class=class="str">"cmt">// FileWrite(handleFile,SymbolName(i,inWatch)、 class=class="str">"cmt">// SymbolInfoString(SymbolName(i,inWatch),SYMBOL_ISIN)、 class=class="str">"cmt">// SymbolInfoString(SymbolName(i,inWatch),SYMBOL_PAGE)、 class=class="str">"cmt">// SymbolInfoString(SymbolName(i,inWatch),SYMBOL_PATH)); } FileClose(handleFile); Alert("File created"); }