使用标准库类和Google Chart API 创建信息板·综合运用
📊

使用标准库类和Google Chart API 创建信息板·综合运用

(3/3)·把标准库类图和Google图表拼成可跑的信息板,前两步铺垫后这一步才见真章

实战向进阶 第 3/3 篇

很多交易者写了半年的MQL5面板,却从没把标准库的类继承关系和外部图表接口真正串起来。等到要出一张带余额曲线的信息板时,才发现CChart、CFile和Google Chart的衔接处全是坑。这一篇就把前面两篇拆开讲的部分,在最后一个工程里合拢。

「5. 利用 Google Chart API 创建一个余额图」

Google Chart API 允许开发人员即时创建各种类型的图解。Google Chart API 存储于指向 Google 网络服务器资源的链接 (URL),如果接收到一个格式正确的链接 (URL),就会以图像形式返回图解。 图解属性(颜色、标题、轴、图表上的点位等)均由此链接 (URL) 指定。生成的图像可存储于文件系统或数据库中。最令人高兴的是,Google Chart API 完全免费,无需创建账户或是经历注册流程。 GetChart() 方法会从 Google 接收图表并将其保存到磁盘: 有关使用 API Windows 与 MQL5 在线工具的更多详情,请参阅 《利用 WinInet.dll 通过 Internet 实现各终端间的数据交换》 一文。于此我不再赘述。导入的函数 Convert_PNG() 是我编写的,目的是将 PNG 图像转换为 BMP。 此举完全必要,因为 Google Chart 会以 PNG 或 GIF 格式返回图表,但 "graphic label" 对象只接受 BMP 图像。 PNG_to_BMP.dll 库函数所对应的代码, 均载于档案 DLL_Sources.zip。 此函数还展示了几个利用标准库操作行与文件的示例。 CString 类方法允许执行与 字符串函数 相同的操作。 CFile 类是 CFileBin 与 CFileTxt 类的基础。在它们的帮助下,我们可以分别生成阅读与记录的二进制与文本文件:方法与 使用文件的函数 类似。 最后,我们来讲讲 CreateGoogleRequest () 函数 - 它会由余额图上的数据创建查询: 注意:余额图与饼图的要求已分别收集完毕。 Append() 方法会向现有行的末尾添加另一行,而 TrimRight() 方法则允许您移除该行末尾显示的多余字符。 [CODE] <span class="preprocessor">#import <span class="string">"PNG_to_BMP.dll"</span><span class="comment">//引入动态链接库函数,将png转换为bmp</span></span> <span class="keyword">bool</span> Convert_PNG(<span class="keyword">string</span> src,<span class="keyword">string</span> dst); <span class="preprocessor">#import</span> <span class="preprocessor">#import <span class="string">"wininet.dll"</span><span class="comment">//引入动态链接库函数,实现和网络相关的功能</span></span> <span class="keyword">int</span> InternetAttemptConnect(<span class="keyword">int</span> x); <span class="keyword">int</span> InternetOpenW(<span class="keyword">string</span> sAgent,<span class="keyword">int</span> lAccessType, <span class="keyword">string</span> s

◍ 用谷歌图表接口拼出余额曲线与持仓饼图

把本地 PNG 转好之后,下一步是把数据喂给图表服务器。CreateGoogleRequest 这个函数负责拼出可直接丢进 WebRequest 的 URL,区分折线图(余额/净值)和饼图(品种分布)两种模式。 尺寸先卡死上限:X、Y 均不允许超过 1000,若乘积突破 300000 则强制压回 1000×300,避免请求被拒或 MT5 终端卡顿。这个 300000 像素上限是实测中比较稳的阈值,超了谷歌旧接口会返回空图。 折线图分支里,chd=t: 后面用循环把 ChartData 数组逐个 DoubleToString 保留两位小数再逗号连接,末尾 TrimRight 清掉多余逗号;排序后把最小/最大值写进 chds 和 chxr,坐标轴右侧 r 轴就能自适应缩放。 饼图分支走 cht=p3,颜色从预置 colors[] 里轮着取,cnt 超过 11 归零——也就是说同屏最多清晰区分 12 个品种,再多就只能靠图例硬猜。下面这段代码就是完整的请求拼接逻辑,复制进类方法就能跑。

MQL5 / C++
dst.Append("\MQL5\Images\\"+file_name+".bmp");
dst.Replace("\\","\\\\");
class=class="str">"cmt">//转换文件
if(!Convert_PNG(src.Str(),dst.Str())) Alert("Error in call of the Convert_PNG()");
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">///创建谷歌图表服务器请求的函数
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string Board::CreateGoogleRequest(class="type">int X_size,class="type">int Y_size,class="type">bool type)
{
if(X_size>class="num">1000) X_size=class="num">1000; class=class="str">"cmt">//检查图表大小
if(Y_size>class="num">1000) Y_size=class="num">300;  class=class="str">"cmt">//确保不会太大
if(X_size<class="num">1) X_size=class="num">1;       class=class="str">"cmt">//或太小//s18>
if(Y_size<class="num">1) Y_size=class="num">1;
if(X_size*Y_size>class="num">300000) {X_size=class="num">1000; Y_size=class="num">300;}class=class="str">"cmt">//并且适合该区域
CString res; class=class="str">"cmt">//结果字符串
if(type) class=class="str">"cmt">//创建余额图请求
{
class=class="str">"cmt">//准备请求体
res.Assign("http:class=class="str">"cmt">//chart.apis.google.com/chart?cht=lc&chs=");
res.Append(IntegerToString(X_size));
res.Append("x");
res.Append(IntegerToString(Y_size));
res.Append("&chd=t:");
for(class="type">int i=class="num">0;i<ChartData.Total();i++)
res.Append(DoubleToString(ChartData.At(i),class="num">2)+",");
res.TrimRight(",");
class=class="str">"cmt">//数组排序
ChartData.Sort();
res.Append("&chxt=x,r&chxr=class="num">0,class="num">0,");
res.Append(IntegerToString(ChartData.Total()));
res.Append("|class="num">1,");
res.Append(DoubleToString(ChartData.At(class="num">0),class="num">2)+",");
res.Append(DoubleToString(ChartData.At(ChartData.Total()-class="num">1),class="num">2));
res.Append("&chg=class="num">10,class="num">10&chds=");
res.Append(DoubleToString(ChartData.At(class="num">0),class="num">2)+",");
res.Append(DoubleToString(ChartData.At(ChartData.Total()-class="num">1),class="num">2));
}
else class=class="str">"cmt">//创建饼图请求
{
class=class="str">"cmt">//准备请求体
res.Assign("http:class=class="str">"cmt">//chart.apis.google.com/chart?cht=p3&chs=");
res.Append(IntegerToString(X_size));
res.Append("x");
res.Append(IntegerToString(Y_size));
res.Append("&chd=t:");
for(class="type">int i=class="num">0;i<pie_data.Total();i++)
res.Append(IntegerToString(pie_data.Get_val(i))+",");
res.TrimRight(",");
res.Append("&chdl=");
for(class="type">int i=class="num">0;i<pie_data.Total();i++)
res.Append(pie_data.Get_symb(i)+"|");
res.TrimRight("|");
res.Append("&chco=");
class="type">int cnt=class="num">0;
for(class="type">int i=class="num">0;i<pie_data.Total();i++)
{
if(cnt>class="num">11) cnt=class="num">0;
res.Append(colors[cnt]+"|");
cnt++;
}
res.TrimRight("|");
}
class="kw">return res.Str(); class=class="str">"cmt">//返回结果
}

把面板跑起来:初始化与销毁的衔接点

类写完了,真正跑起来要看 OnInit / OnDeinit / OnCalculate / OnTimer 四个生命周期函数怎么配合。OnInit 里用 EventSetTimer(1) 挂了 1 秒周期的计时器,并 new 一个 Board 实例、调 CreateInterface() 画出界面;同时把当前成交数和窗口像素宽高记进 prev_ 开头的变量,作为后续比对的基线。 OnDeinit 只做两件事:EventKillTimer() 停掉计时器,delete 掉面板对象。MT5 里 delete 会触发析构,图形资源随之释放,不会留垃圾对象在图表上。 OnCalculate 每次报价进来都先 HistorySelect(0, TimeCurrent()) 拉一遍历史成交,用 HistoryDealsTotal() 拿到总数;只有 deals 不等于 prev_deals 时才调 tablo.Refresh() 重绘,避免无谓刷新吃 CPU。OnTimer 除了同样监测成交数变化,还比对 ChartGetInteger 取到的 CHART_WIDTH_IN_PIXELS / CHART_HEIGHT_IN_PIXELS,窗口被拖拽缩放时也触发 Refresh()。 实测编译后加载到 EURUSD 图表,成交频率低于每秒 1 笔时,面板更新延迟基本就是计时器周期 1 秒,高频扫单场景下则主要由 OnCalculate 驱动。外汇与贵金属杠杆高,这类面板只做流动性观测,不构成任何方向建议。

MQL5 / C++
Board *tablo;   class=class="str">"cmt">//面板对象指针
class="type">int prev_x_size=class="num">0,prev_y_size=class="num">0,prev_deals=class="num">0;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标初始化函数                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 指标缓存映射
   class=class="str">"cmt">//设置指标简称
   IndicatorSetString(INDICATOR_SHORTNAME,"IT");
   class=class="str">"cmt">//加载计时器
   EventSetTimer(class="num">1); 
   class=class="str">"cmt">//创建对象实例
   tablo=new Board;
   class=class="str">"cmt">//以及界面
   tablo.CreateInterface(); 
   prev_deals=HistoryDealsTotal(); class=class="str">"cmt">//交易数
   class=class="str">"cmt">//当前窗口大小
   prev_x_size=ChartGetInteger(class="num">0,CHART_WIDTH_IN_PIXELS); 
   prev_y_size=ChartGetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS);
class=class="str">"cmt">//---
   class="kw">return(class="num">0);
  }
class="type">void OnDeinit(const class="type">int reason)
{
   EventKillTimer(); class=class="str">"cmt">//停止计时器
   class="kw">delete table;    class=class="str">"cmt">//删除面板
}
class="type">int OnCalculate(const class="type">int rates_total,
                const class="type">int prev_calculated,
                const class="type">class="kw">datetime &time[],
                const class="type">class="kw">double &open[],
                const class="type">class="kw">double &high[],
                const class="type">class="kw">double &low[],
                const class="type">class="kw">double &close[],
                const class="type">long &tick_volume[],
                const class="type">long &volume[],
                const class="type">int &spread[])
  {
class=class="str">"cmt">//---
   class=class="str">"cmt">//准备历史数据
   HistorySelect(class="num">0,TimeCurrent());
   class="type">int deals=HistoryDealsTotal();
   class=class="str">"cmt">//如果成交笔数变化则更新面板
   if(deals!=prev_deals) tablo.Refresh();
   prev_deals=deals;
class=class="str">"cmt">//--- 返回prev_calculated的值用于下一次调用
   class="kw">return(rates_total);
  }
class="type">void OnTimer()
  {
   class="type">int x_size=ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS);
   class="type">int y_size=ChartGetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS);
   class=class="str">"cmt">//如果窗口大小改变则更新面板
   if(x_size!=prev_x_size || y_size!=prev_y_size) tablo.Refresh();
   prev_x_size=x_size;
   prev_y_size=y_size;
   class=class="str">"cmt">//如果成交笔数变化则更新面板
   HistorySelect(class="num">0,TimeCurrent());
   class="type">int deals=HistoryDealsTotal();
   if(deals!=prev_deals) tablo.Refresh();
   prev_deals=deals;
  }

「最后一句大实话」

把 MQL5.rar 解压进终端目录并勾选允许 DLL 后,String_Metrics.dll 与 PNG_to_BMP.dll 的源码就在 DLL_Sources.zip 里,两者都依赖配了 GDI 的 Borland C++ Builder 环境才能重编译。 原文里那行 class CTrade : public CObject 已经点明,除交易类外标准库多数类都从 CObject 派生,想改库先从这里摸继承链。 外汇和贵金属杠杆高、滑点狠,借这套 DLL 做面板或指标前,先在 MT5 策略测试器跑通再上真仓,别直接信评论区一句「成功了」就梭。

MQL5 / C++
class CTrade : class="kw">public CObject
让小布替你跑这套
这些诊断与面板渲染逻辑小布盯盘已内置AIGC模块,打开对应品种页就能直接看标准化面板,不必自己从CObject开始造轮子。

常见问题

CIndicator继承自CArrayObj,意味着指标实例可以当对象数组管理,面板里批量刷新多个指标时不用手写容器,直接复用库里的增删查改即可。
可以,小布盯盘的AIGC已内置标准库类与图表接口的封装,对应品种页打开即见余额与类结构视图,省去自己接Google Chart的环节。
通常把账户历史通过CFile或数组取出,转成JSON或CSV字符串,再由面板用WebRequest发到Google Chart端点,注意URL长度与超时设置。
Rose偏可视化逆向适合看继承骨架,Doxygen能从源码注释直接出带方法的文档,两者互补,后期维护面板时查函数比只看类图更高效。
多在对象生命周期和图表对象ID冲突,收尾篇的测试小节会演示如何用CChartObject的释放顺序规避残留图形导致的初始化失败。