创建一个行情卷播面板:改进版·进阶篇
📘

创建一个行情卷播面板:改进版·进阶篇

第 2/2 篇

「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 对象用重载函数吞字符串和双精度,内部靠 '%' 符号区分百分比。最后价格颜色在函数中直改,系统对外调用方式没变,指标侧只动了高度值设定。

MQL5 / C++
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,说明真实排布靠后续重定位,这里只是先把对象建出来。 外汇和贵金属波动大,这类面板只解决信息密度问题,不替代风控;实盘前先在策略测试器用历史数据验证对象刷新会不会卡顿。

MQL5 / C++
            {
                  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 累加,所以多品种是从左往右流式排开。外汇和贵金属波动受杠杆与消息驱动,这类面板只做参考,实际下单仍以小周期结构为准。

MQL5 / C++
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_windowindicator_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 派生指标里即可验证布局。

MQL5 / C++
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 把它塞进市场报价窗口,再抓当前图表的品种代码,尝试切过去;切失败了就把刚才加进报价的品种删掉,避免污染观察列表。外汇和贵金属波动大、杠杆高,误加一堆品种只会拖慢面板重绘。 两个落地细节:新图表默认沿用旧图时间帧,不会自动跳到你习惯的周期;每次换资产或周期,面板都从列表第一个重画,所以品种数控制在十几个内比较合理,多了刷新明显变慢。下面这段消息处理片段就是点击跳转的关键。

MQL5 / C++
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_WRITEFILE_ANSIFILE_CSV 模式打开文件,分隔符是逗号。若句柄等于 INVALID_HANDLE 就弹窗报错并 return,否则用 for 循环把 SymbolName(i,inWatch) 逐个 FileWrite,最后 FileClose 并提示 File created。

注释里留了 ISIN、PAGE、PATH 的写法,但当前版本只落了纯符号名。你在 MT5 里跑完这个函数,去终端数据文件夹的 Widget 目录就能看到 Config.cfg,用记事本打开应是每行一个品种,例如 EURUSD、XAUUSD。外汇与贵金属波动剧烈,脚本只解决符号管理,不构成任何方向建议。

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

常见问题

把自选列表写进配置文件(如 json 或 ini),初始化时读取;文章里给出了落盘与读取的具体字段结构,照着改路径即可。
给每个 GUI 对象绑一个点击事件,用 ChartOpen 或 SymbolSelect 切到对应品种;面板章节里写了对象 ID 与品种名的映射做法。
小布已内置多品种看板与异动提醒,打开对应品种页就能看到日线涨跌和滚动报价,不用自己堆图表对象。
多半是绘制顺序和坐标计算在 OnInit 里没跑完就刷新;按文章说的先算布局再创建对象,并用 Redraw 控节奏。
主要是事件过滤和重绘节流那几处;原文逐行标了改动点,把不必要的 OnTick 重绘关掉就能降负载。