让图表更有趣:添加背景
概述
许多工作站包含一些代表性的图片用以显示用户的一些信息。 这些图片使工作环境更加美丽和愉悦身心,因为人们总是试图选择贴心和最漂亮的图片作为壁纸。 但当我们打开交易平台时,我们发现它有点无趣。 我们所拥有的只是数字数据的图形表示。
即使看很长一段时间的图片和照片您也不会感到疲倦,但观察价格图表哪怕是几分钟也会很累。 所以,我们如此做,就可以观察和分析图表,而背景中的图片能激励我们,提醒我们一些美好的东西。
计划
首先,我们需要判定针对一件事如何定义整个项目的工作方式:我们是打算不时更改图表背景,还是在整个程序生存期中只用一张图片,所有图表都用相同的图片? 我喜欢把不同的图片配发在不同的图表上。 例如,有些代表我正在交易的资产类型,或者有些暗示我应该在该资产中寻找什么。 因此,生成的编译文件不会有任何内部图片,故我们以后可以选择任何所需的图像。
现在还有另一件事需要定义:我们的图像应该放在哪里? MetaTrader 5 有一个目录结构,我们应依据它来访问内容。 目录树不能在此框架之外使用。 如果我们想稍后访问图像,了解如何使用此结构至关重要。 由于我们计划组织存储,并随时间推移进行维护,我们将在文件目录中创建一个文件夹,并将其命名为 WALLPAPERS。 因此,我们将在不离开该树型目录的情况下访问图像,树型目录的根是 MQL5 目录。
但为什么不把文件放在 IMAGES 文件夹中呢? 在此情况下,我们必须在树型目录中遍历,这将是一项不必要的任务,会令程序的逻辑复杂化。 但是,由于我们努力实现最大简单化,我们将利用 MetaTrader 5 提供的功能。 因此,结构如下所示:
之后,我们将添加如下所示的图像,即我们将从一般背景图像中分离徽标图像。 如果我们炒作多种资产,这样的组织形式会令事情井然有序,因为可能会有很多不同的徽标图像。
这是一个简单而高效的解决方案:在不干扰程序操作的情况下添加所需的图像。 现在,请注意一个重要的细节。 图像必须是位图(BITMAP)格式的。 它们应该是 24 位或 32 位的类型,因为这些格式很容易读取:MetaTrader 5 默认情况下可以读取这些格式,所以我保留了这种方式。 当然,如果您可以编制读取例程,也可以使用其它类型,从而最终获得位图图像。 然而,我认为使用图像编辑器,并将其转换为 24 位或 32 位标准图像,比创建单独的读取函数更容易。 LOGOS 文件夹中的文件遵循相同的原则,但有一些例外,我们将很快看到。
现在我们已经定义了规则,那么我们来继续编码。 该代码遵循面向对象编程(OOP)的原则,因此如果需要,您可以轻松地将其移植到脚本或指标,甚至在需要时将其隔离。
一步步来
代码从一些定义开始:
//+------------------------------------------------------------------+ enum eType {IMAGEM, LOGO, COR}; //+------------------------------------------------------------------+ input char user01 = 30; //Transparency ( 0 a 100 ) input string user02 = "WallPaper_01"; //File name input eType user03 = IMAGEM; //Chart background type //+------------------------------------------------------------------+
在此,我们指示我们将要做什么。 eType 枚举指示背景图形是什么类型:图像、徽标、或颜色。 USER02 这项指定背景的文件名,前提是在 USER03 中选择了图像类型。 USER01 表示背景图像的透明度级别,因为在某些情况下,它可能会干扰图表上的数据可视化。 所以我们使用透明度来最小化这种影响。 透明度值可以在 0% 到 100% 之间:值越高,图像越透明。
应将以下函数添加到程序中:
函数 | 参数 | 在哪里声明函数 | 结果 |
---|---|---|---|
Init(string szName, char cView) | 文件名和所需的透明度级别 | 作为 OnInit 代码中的第一个函数 | 加载指定的位图文件,并以指定的透明度进行渲染 |
Init(string szName) | 只有文件是必需的 | 作为 OnInit 代码中的第一个函数 | 加载指定的位图文件,无任何透明度 |
Resize(void) | 不需要参数 | 在 OnChartEvent 代码里;在 CHARTEVENT_CHART_CHANGE 事件 | 调整相应图表上图像的大小 |
如此,我们来看看如何在主代码中使用这些函数,从下面显示的类初始化开始。 请注意,在这种情况下,用户可以指定透明度级别。 为了校正该值,我们应该从 100 中减去它。
int OnInit() { if(user03 != COR) WallPaper.Init(user03 == IMAGE ? "WallPapers\\" + user02 : "WallPapers\\Logos\\" + _Symbol, (char)(100 - user01)); return INIT_SUCCEEDED; }
请注意,如果使用颜色模式,则不会显示图像。 但请注意三元运算符。 当我们选择图像时,程序将指向 FILES 树型目录中的 WALLPAPER 目录。 当它是 LOGO 时,它还将指向相关位置,但请注意,LOGO 的文件名必须与品种名匹配,否则将产生错误。 这都是连续序列的情况。 但如果您炒作的资产有到期日,则需要添加一个小函数来分隔名称中分割当前序列和过期序列的部分。 通过简单地重命名图像,令其反映当前名称,就可以解决这个问题。 对于那些使用交叉订单的人来说,设置一个分离的品种名称调整例程也许很有趣。
下一个需要注意的函数是:
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id == CHARTEVENT_CHART_CHANGE) WallPaper.Resize(); }
所有的代码都很短,由于我不喜欢把事情复杂化,因为这会令改进或修改系统变得困难重重。 另外,尝试以此作为您的原则。 上述函数将保证图表大小的任何变更都会调用该函数,该函数将调整图像大小,从而始终提供良好的外观,并令图像完全呈现。
我们的类代码具有以下特点:
函数 | 参数 | 结果 |
---|---|---|
MsgError(const eErr err, int fp) | 错误类型和文件描述符 | 关闭文件,并显示相应的错误消息。 |
MsgError(const eErr err) | 错误类型 | 显示相应的错误消息 |
LoadBitmap(const string szFileName, uint &data[], int &width, int &height) | 文件名和数据指针 | 加载所需文件,并以 data[] 格式返回其数据,以及以像素为单位的尺寸。 |
~C_WallPaper() | 不需要参数 | 提供对象类的关闭 |
Init(const string szName, const char cView) | 文件名和透明度级别 | 正确初始化整个类 |
Init(const string szName) | 文件名 | 正确初始化整个类 |
Destroy(void) | 不需要参数 | 终结相应的类 |
Resize(void) | 不需要参数 | 正确调整图像尺寸 |
为了避免代码完全混乱,我将错误处理集中在一个函数当中,如下所示。 如果出现问题,它唯一能做的就是向用户发送消息。 这令事情变得更容易,因为在翻译成另一种语言的情况下,您所需要做的只是在一个例程中更改消息,而无需试图找到所用的每一条消息。
bool MsgError(const eErr err, int fp = 0) { string sz0; switch(err) { case FILE_NOT_FOUND : sz0 = "File not found"; break; case FAILED_READ : sz0 = "Reading error"; break; case FAILED_ALLOC : sz0 = "Memory error"; break; case FAILED_CREATE : sz0 = "Error creating an internal resource"; break; }; MessageBox(sz0, "WARNING", MB_OK); if(fp > 0) FileClose(fp); return false; }
下面的函数读取文件,并将其加载到内存当中。 我们需要的唯一信息是文件名,而函数将填充其余的数据。 最终,您会得到图像的尺寸和图像本身,不过是以位图格式。 注意这一点很重要,因为虽然有若干种格式,格式之间彼此压缩方式不同,但结果最终要转化为位图格式。
bool LoadBitmap(const string szFileName, uint &data[], int &width, int &height) { struct BitmapHeader { ushort type; uint size; uint reserv; uint offbits; uint imgSSize; uint imgWidth; uint imgHeight; ushort imgPlanes; ushort imgBitCount; uint imgCompression; uint imgSizeImage; uint imgXPelsPerMeter; uint imgYPelsPerMeter; uint imgClrUsed; uint imgClrImportant; } Header; int fp; bool noAlpha, noFlip; uint imgSize; if((fp = FileOpen(szFileName + ".bmp", FILE_READ | FILE_BIN)) == INVALID_HANDLE) return MsgError(FILE_NOT_FOUND); if(FileReadStruct(fp, Header) != sizeof(Header)) return MsgError(FAILED_READ, fp); width = (int)Header.imgWidth; height = (int)Header.imgHeight; if(noFlip = (height < 0)) height = -height; if(Header.imgBitCount == 32) { uint tmp[]; noAlpha = true; imgSize = FileReadArray(fp, data); if(!noFlip) for(int c0 = 0; c0 < height / 2; c0++) { ArrayCopy(tmp, data, 0, width * c0, width); ArrayCopy(data, data, width * c0, width * (height - c0 - 1), width); ArrayCopy(data, tmp, width * (height - c0 - 1), 0, width); } for(uint c0 = 0; (c0 < imgSize && noAlpha); c0++) if(uchar(data[c0] >> 24) != 0) noAlpha = false; if(noAlpha) for(uint c0 = 0; c0 < imgSize; c0++) data[c0] |= 0xFF000000; } else { int byteWidth; uchar tmp[]; byteWidth = width * 3; byteWidth = (byteWidth + 3) & ~3; if(ArrayResize(data, width * height) != -1) for(int c0 = 0; c0 < height; c0++) { if(FileReadArray(fp, tmp, 0, byteWidth) != byteWidth) return MsgError(FAILED_READ, fp); else for(int j = 0, k = 0, p = width * (height - c0 - 1); j < width; j++, k+=3, p++) data[p] = 0xFF000000 | (tmp[k+2] << 16) | (tmp[k + 1] << 8) | tmp[k]; } } FileClose(fp); return true; }
请看以下代码行:
if((fp = FileOpen(szFileName + ".bmp", FILE_READ | FILE_BIN)) == INVALID_HANDLE)
请注意,文件扩展名是在此处指定的,即,当我们指示图像是什么时,我们不会指定它,因为如果我们指定扩展名,我们将得到一个“文件未找到”错误。 该函数的其余部分非常简单:首先,它读取文件头,并检查它是 32 位还是 24 位的位图;然后它相应地读取图像,因为 32 位图像的内部结构与 24 位图像略有不同。
下一个函数会针对屏幕上显示的位图逐一初始化图像的所有数据。 请注意,在此函数执行期间,我们将此位图文件转换为程序资源。 这是必要的,因为稍后我们将把这个资源与一个对象链接,而这个对象若要显示在屏幕上,不能作为一个对象,而是作为一个资源。 似乎很难理解我们为什么要这样做。 但这一步允许我们创建同一类型的多个资源,然后将它们与某些需要显示内容的单个对象关联。 如果我们往程序中添加一个特定的资源,我们会将其定义为一个内部资源,并编译文件。 在此情况下,不重新编译源代码就不可能更改资源。 然而,通过创建动态资源,可以指定要使用的资源。
bool Init(const string szName, const char cView = 100, const int iSub = 0) { double dValue = ((cView > 100 ? 100 : (cView < 0 ? 0 : cView)) * 2.55) / 255.0; m_Id = ChartID(); if(!LoadBitmap(szName, m_BMP, m_MemWidthBMP, m_MemHeightBMP)) return false; Destroy(); m_Height = m_MemHeightBMP; m_Width = m_MemWidthBMP; if(ArrayResize(m_Pixels, (m_MemSizeArr = m_Height * m_Width)) < 0) return MsgError(FAILED_ALLOC); m_szRcName = "::" + szName + (string)(GetTickCount64() + MathRand()); if(!ResourceCreate(m_szRcName, m_Pixels, m_Width, m_Height, 0, 0, 0, COLOR_FORMAT_ARGB_NORMALIZE)) return MsgError(FAILED_CREATE); if(!ObjectCreate(m_Id, (m_szObjName = szName), OBJ_BITMAP_LABEL, iSub, 0, 0)) return MsgError(FAILED_CREATE); ObjectSetInteger(m_Id, m_szObjName, OBJPROP_XDISTANCE, 0); ObjectSetInteger(m_Id, m_szObjName, OBJPROP_YDISTANCE, 0); ObjectSetString(m_Id, m_szObjName, OBJPROP_BMPFILE, m_szRcName); ObjectSetInteger(m_Id, m_szObjName, OBJPROP_BACK, true); for(uint i = 0; i < m_MemSizeArr; i++) m_BMP[i] = (uchar(double(m_BMP[i] >> 24) * dValue) << 24) | m_BMP[i] & 0x00FFFFFF; return true; }
这一切都很好,而且看起来很实用,但对象本身没有更改资源的能力。 这意味着,仅仅通过将资源链接到对象,不可能改变资源的工作方式或呈现方式。 有时这样做会让事情变得有点复杂,因为大多数时候我们必须通过编码方式在对象内部修改资源。
到此关键点,即可用以下代码渲染图像:
if(ResourceCreate(m_szRcName, m_Pixels, m_Width, m_Height, 0, 0, 0, COLOR_FORMAT_ARGB_NORMALIZE)) ChartRedraw();
但使用此代码并不能保证图像会按预期显示,除非它拥有精确的图表尺寸。 我建议您使用高清图像。 大尺寸图像表现更好,令计算更容易,并节省处理时间,这在某些情况下可能是至关重要的。 但即便如此,我们仍然存在图像无法正确呈现的问题,因为对象不会更改资源,令其符合对象的规格。 因此,我们必须做一些事情来实现资源的正确建模,从而可用对象来呈现资源。 与图像领域相关的数学包括从最简单的计算,到非常复杂的事情。 但由于处理时间对我们来说至关重要,且因它取用的是价格图表,所以我们不能做过多的计算。 事情应该尽可能简单迅速。 因此,我们可以选用比图表更大的图像,因为唯一需要的计算是缩小尺寸。 我们来看看它会如何操作。
图表上用到的数学关系如下所示:
注意这里的 f(x) = f(y),它保留了图像比率。 这也被称为“纵横比”,意味着图像彻底改变。 但是如果 f(y) 不依赖于 f(x),我们的图像会发生什么呢? 好吧,它会发生不成比例的变化,从而形成任何形状。 虽然我们在缩小尺寸时没有问题,但在增大尺寸时却并非如此:如果 f(x) > 1.0 或 f(y) > 1.0,我们会得到图像缩放,这就是问题的开始。 第一个问题出现在下图中:
从下图中可以看到是因为该效应而发生。 请注意,空白代表图像在放大效果中增长时出现的空白区域。 当 f(x) 或 f(y) 大于 1 时,即当我们遵循红色箭头时,总会发生这种情况。 在下图中,f(x) = f(y) = 2.0,即我们将图像放大 2 倍。
有若干种方法可以解决这个问题,其中之一是在找到空白块时进行插值。 在此时刻,我们应该进行因式分解,并计算所用颜色之间的过渡颜色,从而产生平滑效果,并填充空白点。 但有一个与计算关联的问题。 即使插值完成很快,这可能也不适合 MetaTrader 5 图表的实时数据。 即使屏幕上的图表在整体时间内进行了几次尺寸调整(因为在大多数情况下,图表尺寸会小于图像,在这种情况下,f(x) 和 f(y) 将等于或小于 1.0,插值此刻没有效果),但如果您考虑在相同尺寸的屏幕上使用图像大小为 1920 x 1080(全高清)的屏幕,插值会显著增加处理时间,但对最终结果没有任何益处。
下面我们来看看在一个尺寸倍增的图像上是如何进行插值计算的。 显然它会非常快速,但别忘了这应该是针对 32 位的颜色方案(或 ARGB)完成,我们有 4 个字节的 8 比特位进行计算。 GPU 的函数能够快速执行这些计算,但通过 OpenCL 访问这些函数可能不会带来任何实际好处,因为我们会延迟 GPU 的数据输入和输出。 因此,由 GPU 执行这种计算,速度上没有任何优势。
考虑到这一点,我认为由于平滑效果导致的图像质量稍差并不是什么大问题;因为在大多数情况下,只有在 4k 屏幕上使用全高清图像时,f(x) 或 f(y) 才不会高于 2。 在此场景下,平滑很轻微,几乎看不到。 因此,替代插值点,我宁愿将一个点拖动到下一处,快速填充空值,如此计算成本降至最低。 其操作方式如下所示。 由于我们只是简单地复制数据,我们可以在一个步骤中处理所有 32 位,速度将与图形处理系统的速度一样快。
因此,此处是能够快速调整图像尺寸的函数。
void Resize(void) { m_Height =(uint) ChartGetInteger(m_Id, CHART_HEIGHT_IN_PIXELS); m_Width = (uint) ChartGetInteger(m_Id, CHART_WIDTH_IN_PIXELS); double fx = (m_Width * 1.0) / m_MemWidthBMP; double fy = (m_Height * 1.0) / m_MemHeightBMP; uint pyi, pyf, pxi, pxf, tmp; ArrayResize(m_Pixels, m_Height * m_Width); ArrayInitialize(m_Pixels, 0x00FFFFFF); for (uint cy = 0, y = 0; cy < m_MemHeightBMP; cy++, y += m_MemWidthBMP) { pyf = (uint)(fy * cy) * m_Width; tmp = pyi = (uint)(fy * (cy - 1)) * m_Width; for (uint x = 0; x < m_MemWidthBMP; x++) { pxf = (uint)(fx * x); pxi = (uint)(fx * (x - 1)); m_Pixels[pxf + pyf] = m_BMP[x + y]; for (pxi++; pxi < pxf; pxi++) m_Pixels[pxi + pyf] = m_BMP[x + y]; } for (pyi += m_Width; pyi < pyf; pyi += m_Width) for (uint x = 0; x < m_Width; x++) m_Pixels[x + pyi] = m_Pixels[x + tmp]; } if (ResourceCreate(m_szRcName, m_Pixels, m_Width, m_Height, 0, 0, 0, COLOR_FORMAT_ARGB_NORMALIZE)) ChartRedraw(); }
函数有一个嵌套循环,内部循环执行函数 f(x),,外部循环 - f(y)。 当执行 f(x) 时,我们可以有空白区域 — 这在下面的代码行中得到了修复:
for (pxi++; pxi < pxf; pxi++) m_Pixels[pxi + pyf] = m_BMP[x + y];
如果 X 值之间出现差异,上面的一行将通过复制图像的最后一个值来修复。 后果就是,我们将使用别名,但在这些情况下,计算成本将会最小,因为在执行该代码片段时,将有一个内部循环在最短时间内运行(情况并非总是如此)。 如果您希望插值数据不产生这种锯齿效应,只需修改这一行,即可创建如上计算。
一旦整行计算完毕,如果 f(y) 大于1,则检查 f(y),以避免出现空白区域。 这是通过行完成的:
for (pyi += m_Width; pyi < pyf; pyi += m_Width) for (uint x = 0; x < m_Width; x++) m_Pixels[x + pyi] = m_Pixels[x + tmp];
同样,这将导致别名,但这可以如同修改前一行代码相同的方式进行修复。 我们添加新图像的宽度值,因为我们正在复制一行,该行已由负责处理新图像的 f(x) 在循环中得以处理。 如果用任何其它数值完成此操作,图像将以奇怪的方式扭曲。
结束语
我希望这个思路会令您的图表变得更加有趣和生动,可以看上好几个小时;因为当背景图像变得很无趣时,您可以简单地选择另一个,而无需重新编译任何东西。 只需选择图表背景所要显示的新图像。
这里要提到的最后一个细节是:如果您想在 EA 中使用背景图片置换类,那么它必须是在 INIT 例程中声明的第一件事。 这将防止背景图像与 EA 创建的其它图形对象重叠。
尽享最终的表现结果,您现在会深深地沉浸在图表研究分析之中...
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10215
。
因此,在MT5的更新中发生了一些变化,使墙纸变成了与预期不同的东西,也就是文章中所描述的那样,但这并不意味着它是由基金公司提供的,因为在C_WallPaper类别中,你可以看到以下的内容。
这说明对象必须要停在基座上,更明显的是,它被挡在了前面,因为接收的对象或BitMap会接收这些小块,一个解决办法是提高所有对象的状态,或者尝试降低Bitmap的状态,如果是这样,第二种办法就更简单了,这可以改变接收墙纸的对象的OBJPROP_ZORDER属性的值。我曾尝试过这两种解决方案,但我没有找到稳定的方法来解决这个问题,而且,我也不知道该怎么做,因为纸质的纸张必须按小时来计算。..如果你注意看,你会发现位图图像将被绘制在蜡烛的主体上,表明位图对象处于前台,这同样不是预期的行为,因为上面的那行代码,因为这个原因,它刚好接收到任何和所有的点击事件......🙁
请在点击图片时修改图片。我想知道是否有可能,如果没有可能,那么我就不会放弃我的神经系统。
在MT5的更新中发生了一些事情,将墙纸变成了与文章中预期不同的东西,因为它正确地显示在图形背景中,因为在C_WallPaper类中你会发现以下一行。
这告诉我们,该对象将不得不留在后台,更奇怪的是,它正在走到前面,因为这个,接收或BitMap的OBJECT开始接收点击,一个解决方案是增加所有对象的状态,或尝试降低Bitmap的状态,在这种情况下,这第二个会更简单。这将通过改变接收墙纸的对象中的OBJPROP_ZORDER属性的值来实现,我尝试了这两种解决方案,但我无法以一种纠正问题的方式稳定整个事情,因此,和INFELIZMENTE,墙纸必须暂时被丢弃。..如果你注意看,你会发现位图图像被绘制在蜡烛体上,表明位图对象在前台,这同样不是预期的行为,因为上面那行代码,正因为如此,它刚好接收到任何和所有的点击事件......。🙁
丹尼尔。
如果你放一个定时器并运行一个功能来禁用和再次启用墙纸,或者通过把墙纸放在后台(后面)来 "重申 "这个功能,这可能吗?
我使用的一个GUI面板,我不得不去选择这个解决方案。一些图形元素需要删除并重新创建,根据对象的类型,放在背景或前面。