基于画布的指标:为通道填充透明度
概述
在本文中,我将介绍一种创建自定义指标的方法,其绘图是利用标准库中的 CCanvas 类制作的。 我将着手处理特殊的指标,其需要用一种纯色填充两条线之间的区域。 在开始之前,我们将了解为什么要用画布,这也许是此类指标当前可用选项的最佳选择。 之后,我们将看到计算坐标所需的一些图表属性,以及涉及操控 CCanvas 的基本过程。
最终目标是结合到目前为止看到的所有内容来构建应用透明度的指标。 所有工作将只考虑在主图表窗口。 一旦我们的目标达成,我们就可以将其扩展到在子窗口里工作的指标。
本文的主题如下:
采用画布的原因
有人会问,为什么要采用画布替代已在自定义指标中采用的 DRAW_FILLING? 这里至少有两个原因:
- 指标的颜色与其它指标、蜡烛和图表对象的颜色混杂
- DRAW_FILLING 不支持透明度
图表窗口属性
为了开始绘制自定义图表,我们需要研究一些图表属性。 可以在文档中找到所有属性 若要获取这些属性值,我们需要调用相应的函数 ChartGetInteger 和 ChartGetDouble。 还有一个 ChartGetString,但我们在这里不会用到它。
我们打算把用到的属性按简述列出。 如果我们需要更多,我稍后会一并列出。
- CHART_WIDTH_IN_PIXELS — 图表窗口的宽度,不包括价格标尺
- CHART_HEIGTH_IN_PIXELS — 子窗口的高度,不包括日期标尺
- CHART_PRICE_MAX — 对应于子窗口顶部的价格
- CHART_PRICE_MIN — 对应于子窗口底部的价格
- CHART_SCALE — 确定柱线之间的间距。 经过一些测试,我发现它是两个值的幂,由 pow(2, CHART_SCALE) 得到。
- CHART_FISRT_VISIBLE_BAR — 图表上第一根可见的柱线,从左到右。
- CHART_VISIBLE_BARS — 图表上可见柱线的数量。
理解图表窗口属性
这些属性可在下图中轻松看到。
属性 CHART_WIDTH_IN_PIXELS 和 CHART_HEIGTH _IN_PIXELS,我们将用它们来确定我们需要创建的画布对象的大小,以便制作绘图。 当图表窗口发生变化时,如果这些属性也发生了变化,我们就需要调整画布大小。
为了更好地理解,我们将创建一个简单的指标,显示属性,以及它们如何根据价格变化和用户交互而变化。 我们已开始采用画布来了解指标绘制过程。
图表属性查看器指标
此刻,假设您已经知道如何创建自定义指标。 如果您还不懂,您可以从这篇文章 MQL5:创建您自己的指标 和 探索创建多色烛条的选项开始。 我们开始吧。
我已遵循这条路径创建了我的指标。 出于组织原则目的,我建议您也这样做。
一旦指标框架准备就绪,我们需要将 CCanvas 函数库添加到文件当中。 我们可以使用 #include 预编译指令来做到这一点。
然后我们创建 CCanvas 类的实例。 所有这些都紧跟在指标 #property 指令之后。
#property copyright "Copyright 2023, Samuel Manoel De Souza" #property link "https://www.mql5.com/en/users/samuelmnl" #property version "1.00" #property indicator_chart_window #include <Canvas/Canvas.mqh> CCanvas Canvas;
操控 CCanvas 时,我们需要做的第一件事是创建一个 OBJ_BITMAP_LABEL,并向其内附加资源。 如果您想将其添加到图表之中,通常在指标初始化里,调用 CreateBitampLabel(...) 方法,即可完成。 最后是删除 OBJ_BITMAP_LABEL 和附加到它的资源。 如果您想从图表中删除它,通常在指标逆初始化里,调用 Destory(void) 方法,即可完成。 与此同时,我们执行基本的绘图过程,其中包括擦除图形(清除或设置资源的默认像素值),制作图形,并更新资源。 画布流程的完整生存周期如下图所示。
为简单起见,我们将在一个名为 “Redraw” 的函数中暂留 "Erase","Draw","Update"。 在代码中编写所有内容,我们得到以下结构。
//+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- indicator buffers mapping Canvas.CreateBitmapLabel(0, 0, "Canvas", 0, 0, 200, 150, COLOR_FORMAT_ARGB_NORMALIZE); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Custom indicator deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { Canvas.Destroy(); } //+------------------------------------------------------------------+ //| Custom indicator redraw function | //+------------------------------------------------------------------+ void Redraw(void) { uint default_color = ColorToARGB(clrBlack); uint text_color = ColorToARGB(clrWhite); //--- canvas erase Canvas.Erase(default_color); //--- add first draw //--- add second draw //--- add ... draw //--- add last draw //--- canvas update Canvas.Update(); }
为了显示属性,我们将使用 TextOut 方法编写它们。 而这些属性值将以字符串形式存储在结构数组变量当中
struct StrProperty { string name; string value; };结构可以如下。 然后我们可以在循环中输出它们的摘要。 由于我们还没有数组,我们将在 Redraw 函数中将数组作为参数传递。 然后,重绘函数将如下所示:
void Redraw(StrProperty &array[]) { uint default_color = ColorToARGB(clrBlack); uint text_color = ColorToARGB(clrWhite); //--- canvas erase Canvas.Erase(default_color); //--- add first draw int total = ArraySize(array); for(int i=0;i<total;i++) { int padding = 2; int left = padding, right = Canvas.Width() - padding, y = i * 20 + padding; Canvas.TextOut(left, y, array[i].name, text_color, TA_LEFT); Canvas.TextOut(right, y, array[i].value, text_color, TA_RIGHT); } //--- canvas update Canvas.Update(); }最后我们就可以获取属性值,并输出它们。 如果您的代码还没有 OnChartEvent 函数处理程序,则您需要将其加入。 在那里,我们将检查 CHARTEVENT_CHART_CHANGE 事件 ID。 当有事件发生时,我们声明一些变量来获取属性值,并将它们传递给结构数组,然后调用 Redraw 函数。 那么我们启程吧。 我们可以编译指标,将其添加到图表中,并操作图表来查看画布更新。
//+------------------------------------------------------------------+ //| Custom indicator chart event handler function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { if(id != CHARTEVENT_CHART_CHANGE) return; int chart_width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); int chart_height = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); int chart_scale = (int)ChartGetInteger(0, CHART_SCALE); int chart_first_vis_bar = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR); int chart_vis_bars = (int)ChartGetInteger(0, CHART_VISIBLE_BARS); double chart_prcmin = ChartGetDouble(0, CHART_PRICE_MIN); double chart_prcmax = ChartGetDouble(0, CHART_PRICE_MAX); //--- StrProperty array[] { {"Width", (string)chart_width}, {"Height", (string)chart_height}, {"Scale", (string)chart_scale}, {"First Vis. Bar", (string)chart_first_vis_bar}, {"Visible Bars", (string)chart_vis_bars}, {"Price Min", (string)chart_prcmin}, {"Price Max", (string)chart_prcmax}, }; Redraw(array); }
坐标转换
此处,我们需要一些基本函数来从日期时间或柱线索引转换为像素坐标 x,以及把价格转换为像素坐标 y,或从 x 转换为柱线索引,以及从 y 转换为价格(其中一些我们现在不会用到,但我们可以一次将它们全部制作完成)。 有因于此,我们将图表属性变量移动到全局范围,而在 OnChartEvent 函数中,我们只会更新数值,并在需要时调用 Redraw 函数。 理想的解决方案是将变量和转换函数封装在类或结构中,但现在我们先保持简单。 不过,我建议您通过阅读文章面向对象编程的基础知识和文档中的相关主题(面向对象编程)来开始学习 OOP。 我们将在下一次机会中用到这些。
由于函数基本上涉及比例关系,因此我们不会花费时间和文字进行解释。 它们如下。
//+------------------------------------------------------------------+ //| Converts the chart scale property to bar width/spacing | //+------------------------------------------------------------------+ int BarWidth(int scale) {return (int)pow(2, scale);} //+------------------------------------------------------------------+ //| Converts the bar index(as series) to x in pixels | //+------------------------------------------------------------------+ int ShiftToX(int shift) {return (chart_first_vis_bar - shift) * BarWidth(chart_scale) - 1;} //+------------------------------------------------------------------+ //| Converts the price to y in pixels | //+------------------------------------------------------------------+ int PriceToY(double price) { // avoid zero divider if(chart_prcmax - chart_prcmin == 0.) return 0.; return (int)round(chart_height * (chart_prcmax - price) / (chart_prcmax - chart_prcmin) - 1); } //+------------------------------------------------------------------+ //| Converts x in pixels to bar index(as series) | //+------------------------------------------------------------------+ int XToShift(int x) { // avoid zero divider if(BarWidth(chart_scale) == 0) return 0; return chart_first_vis_bar - (x + BarWidth(chart_scale) / 2) / BarWidth(chart_scale); } //+------------------------------------------------------------------+ //| Converts y in pixels to price | //+------------------------------------------------------------------+ double YToPrice(int y) { // avoid zero divider if(chart_height == 0) return 0; return chart_prcmax - y * (chart_prcmax - chart_prcmin) / chart_height; }
透明的 DRAW_FILLING
现在,我们拥有利 CCanvas 实现 DRAW_FILLING 所需的一切。
我们不会花时间创建一个新指标。 取而代之,我们举一个存在于所有 MetaTrader 5 平台中的示例,并在两条线之间加上填充。 我将使用位于终端数据文件夹中的目录 “\\MQL5\\指标\\Examples\\” 中找到的轨道线(Envelopes)。 我将 Envelopes.mq5 复制到我创建的 ChartPropertiesViwer 指标的同一目录之中。 您可以选择任何指标,但我建议在跟随本文讲述的步骤时选择相同的指标。
我们需要做的第一件事是将我们在 ChartPropertiesViewer 指标中所做的一切都复制到轨道线。
如上所述,我们将填充两条线之间的通道。 为此,我们将创建一个函数,其中将传递与这些行值对应的数组。 在轨道线指标中,数组由变量 ExtUpBuffer 和 ExtMABuffer 给出。
double ExtUpBuffer[]; double ExtDownBuffer[];
不光数组,我们还需传递更多变量,这些变量将允许我们使用两种颜色,设置透明度级别,并将指标移动到图表的左侧或右侧。
参数 | 变量说明 |
---|---|
serie1 | 对应于第一条线的数值数组 |
serie2 | 对应于第二条线的数值数组 |
clr1 | 当 serie1 >= serie2 时的颜色 |
clr2 | 当 serie1 < serie2 时的颜色 |
alpha | 通道的透明度的数值 |
plot_shift | 指标向右或向左的偏移值 |
使用现有变量和上述参数的函数如下。
//+------------------------------------------------------------------+ //| Fill the area between two lines | //+------------------------------------------------------------------+ void DrawFilling(double &serie1[], double &serie2[], color clr1, color clr2, uchar alpha = 255, int plot_shift = 0) { int start = chart_first_vis_bar; int total = chart_vis_bars + plot_shift; uint argb1 = ColorToARGB(clr1, alpha); uint argb2 = ColorToARGB(clr2, alpha); int limit = fmin(ArraySize(serie1), ArraySize(serie2)); int px, py1, py2; for(int i = 0; i < total; i++) { int bar_position = start - i; int bar_shift = start - i + plot_shift; int bar_index = limit - 1 - bar_shift; if(serie1[bar_index] == EMPTY_VALUE || serie1[bar_index] == EMPTY_VALUE || bar_shift >= limit) continue; int x = ShiftToX(bar_position); int y1 = PriceToY(serie1[bar_index]); int y2 = PriceToY(serie2[bar_index]); uint argb = serie1[bar_index] < serie2[bar_index] ? argb2 : argb1; if(i > 0 && serie1[bar_index - 1] != EMPTY_VALUE && serie2[bar_index - 1] != EMPTY_VALUE) { if(py1 != py2) Canvas.FillTriangle(px, py1, px, py2, x, y1, argb); if(y1 != y2) Canvas.FillTriangle(px, py2, x, y1, x, y2, argb); } px = x; py1 = y1; py2 = y2; } }
到目前为止,我们一直在使用固定尺寸的画布。 然而,指标绘图需要画布来填充图表的整个区域。 更重要的是,每当图表窗口大小发生变化时,通过最大化、最小化、拉伸到任何一侧或添加子窗口指标,我们需要确保画布仍然填满图表的整个区域。 为此,我们将调整画布的大小,在 OnChartEvent 函数中引入一处小修改。
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { if(id != CHARTEVENT_CHART_CHANGE) return; chart_width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); chart_height = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); chart_scale = (int)ChartGetInteger(0, CHART_SCALE); chart_first_vis_bar = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR); chart_vis_bars = (int)ChartGetInteger(0, CHART_VISIBLE_BARS); chart_prcmin = ChartGetDouble(0, CHART_PRICE_MIN, 0); chart_prcmax = ChartGetDouble(0, CHART_PRICE_MAX, 0); if(chart_width != Canvas.Width() || chart_height != Canvas.Height()) Canvas.Resize(chart_width, chart_height);
现在我们将进行一些小更新,从而令其正常工作。
- 更新我们的 Redraw 函数,删除先前指标中添加的参数,并添加 DrawFilling 函数。
- 在 OnComputing 中添加我们的 Redraw 函数,以便在指标值发生变化时更新绘图。
- 更改在调用 CreateBitmapLabel 时作为参数传递的对象名称。
//+------------------------------------------------------------------+ //| Custom indicator redraw function | //+------------------------------------------------------------------+ void Redraw(void) { uint default_color = 0; color clrup = (color)PlotIndexGetInteger(0, PLOT_LINE_COLOR, 0); color clrdn = (color)PlotIndexGetInteger(1, PLOT_LINE_COLOR, 0); //--- canvas erase Canvas.Erase(default_color); //--- add first draw DrawFilling(ExtUpBuffer, ExtDownBuffer,clrup, clrdn, 128, InpMAShift); //--- canvas update Canvas.Update(); } //--- the main loop of calculations for(int i=start; i<rates_total && !IsStopped(); i++) { ExtUpBuffer[i]=(1+InpDeviation/100.0)*ExtMABuffer[i]; ExtDownBuffer[i]=(1-InpDeviation/100.0)*ExtMABuffer[i]; } Redraw(); //--- OnCalculate done. Return new prev_calculated. return(rates_total);
Canvas.CreateBitmapLabel(0, 0, short_name, 0, 0, 200, 150, COLOR_FORMAT_ARGB_NORMALIZE);
这样就完成了。 您可以看到它现在有两个不同周期的轨道线,和一个矩形对象的外观。
如您所见,指标问题已解决,而图表对象的问题仍然存在,但这是另一篇文章要解决的问题。
在子窗口指标中工作的扩展方法
参见下面的图片。 在这里,我们可以看到一个使用 DRAW_FILLING 的子窗口指标。 这张照片取自 MQL 文档。 我们将做同样的事情,但允许 CCanvas 使用透明度,更重要的是,避免重叠区域问题。
下面列出了我们需要做的修改:
- 在指标所在的同一子窗口中创建位图标签
- 根据子窗口替代主图表窗口的大小调整画布大小
若要在同一子窗口中创建位图标签,并获取子窗口的大小,我们需要找到指标放置在哪个子窗口之中。 我们可以认为它只是图表的最后一个子窗口,但终端允许在同一子窗口上放置两个或多个指标,不一定是最后一个子窗口。 然后我们需要一个函数来返回指标所在的子窗口的编号。 看看下面的函数:
//+------------------------------------------------------------------+ //| return the number of the subwindow where the indicator is located| //+------------------------------------------------------------------+ int ChartIndicatorFind(string shortname) { int subwin = ChartGetInteger(0, CHART_WINDOWS_TOTAL); while(subwin > 0) { subwin--; int total = ChartIndicatorsTotal(0, subwin); for(int i = 0; i < total; i++) { string name = ChartIndicatorName(0, subwin, i); if(name == shortname) return subwin; } } return -1; }
在最后一个指标中,我们已拿轨道线指标作为示例。 现在,我们将借助文档中的代码,主题 DRAW_FILLING 作为示例的源代码。 我们可以在之前创建两个指标的同一目录中再创建一个新指标。 我们将其命名为 “SubwindowIndicator”。 然后从文档中复制代码。
该指标使用 DRAW_FILLING 绘制。 由于我们将使用 CCanvas 来填充通道,因此我们可以用曲线替换绘图类型。 以下是指标属性当中的变化。
#property indicator_plots 2 //--- plot Intersection #property indicator_label1 "Fast" #property indicator_type1 DRAW_LINE #property indicator_color1 clrRed #property indicator_width1 1 #property indicator_label2 "Slow" #property indicator_type2 DRAW_LINE #property indicator_color2 clrBlue #property indicator_width2 1
以及 OnInit 函数中的变化。
//--- indicator buffers mapping SetIndexBuffer(0,IntersectionBuffer1,INDICATOR_DATA); SetIndexBuffer(1,IntersectionBuffer2,INDICATOR_DATA); //--- PlotIndexSetInteger(0,PLOT_SHIFT,InpMAShift); PlotIndexSetInteger(1,PLOT_SHIFT,InpMAShift);
此外,我们不需要更改指标线条外观,那么我们可以在 OnCompute 函数中注释此行。
//--- If a sufficient number of ticks has been accumulated if(ticks>=N) { //--- Change the line properties //ChangeLineAppearance(); //--- Reset the counter of ticks to zero ticks=0; }
现在我们可以添加图表属性变量和本文中创建的函数。 在此指标中,我们需要作为参数传递给 DrawFill 函数的数组具有不同的名称。 如此,我们需要在 Redraw 函数中更改它
double IntersectionBuffer1[]; double IntersectionBuffer2[];
Redraw 函数变为:
//+------------------------------------------------------------------+ //| Custom indicator redraw function | //+------------------------------------------------------------------+ void Redraw(void) { uint default_color = 0; color clrup = (color)PlotIndexGetInteger(0, PLOT_LINE_COLOR, 0); color clrdn = (color)PlotIndexGetInteger(1, PLOT_LINE_COLOR, 0); //--- canvas erase Canvas.Erase(default_color); //--- add first draw DrawFilling(IntersectionBuffer1, IntersectionBuffer2, clrup, clrdn, 128, InpMAShift); //--- canvas update Canvas.Update(); }
编译代码后,我们得到了预期的结果。
结束语
在本文中,我们已经了解了涉及使用 CCanvas 的基本过程、一些图表属性、如何获取它们的值,并用它们进行一些有用的基本坐标转换,以便适用于多种用途。 之后,我们就能够开发一个拥有透明度的指标。 扩展到子窗口指标的方法是我们的最后任务。
本文中开发的指标文件可以在本文末尾找到以供下载。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/12357