让图表更有趣:添加背景·进阶篇
把 BMP 头读进结构体的坑
在 MT5 里用 MQL5 直接解析位图,第一步是把 54 字节的 BITMAPINFOHEADER 原样映射到结构体。下面这段先定义了错误回显函数,再开文件、读头,任何一步失败就弹窗并关句柄。 MsgError 用 switch 把四种枚举错误转成英文串,MessageBox 标题写死 WARNING,fp>0 时才 FileClose,避免重复关句柄导致后续文件操作异常。 LoadBitmap 里的 BitmapHeader 严格按 BMP 规范排字段:type 是 ushort 的 'BM',offbits 通常是 54,imgWidth/imgHeight 是 uint。FileOpen 拼了 .bmp 后缀且用 FILE_READ|FILE_BIN,返回 INVALID_HANDLE 直接报 FILE_NOT_FOUND。 FileReadStruct 的返回值必须和 sizeof(Header) 比,在 x64 编译器下这个 Header 占 54 字节;不等就说明文件截断或不是标准 BMP,立刻 FAILED_READ 退出。开 MT5 把这段贴进 EA,故意传个不存在的文件名,能看到弹窗报 File not found。
class="type">bool MsgError(class="kw">const eErr err, class="type">int fp = class="num">0) { class="type">class="kw">string sz0; class="kw">switch(err) { case FILE_NOT_FOUND : sz0 = "File not found"; class="kw">break; case FAILED_READ : sz0 = "Reading error"; class="kw">break; case FAILED_ALLOC : sz0 = "Memory error"; class="kw">break; case FAILED_CREATE : sz0 = "Error creating an internal resource"; class="kw">break; }; MessageBox(sz0, "WARNING", MB_OK); if(fp > class="num">0) FileClose(fp); class="kw">return false; } class="type">bool LoadBitmap(class="kw">const class="type">class="kw">string szFileName, class="type">uint &data[], class="type">int &width, class="type">int &height) { class="kw">struct BitmapHeader { class="type">class="kw">ushort type; class="type">uint size; class="type">uint reserv; class="type">uint offbits; class="type">uint imgSSize; class="type">uint imgWidth; class="type">uint imgHeight; class="type">class="kw">ushort imgPlanes; class="type">class="kw">ushort imgBitCount; class="type">uint imgCompression; class="type">uint imgSizeImage; class="type">uint imgXPelsPerMeter; class="type">uint imgYPelsPerMeter; class="type">uint imgClrUsed; class="type">uint imgClrImportant; } Header; class="type">int fp; class="type">bool noAlpha, noFlip; class="type">uint imgSize; if((fp = FileOpen(szFileName + ".bmp", FILE_READ | FILE_BIN)) == INVALID_HANDLE) class="kw">return MsgError(FILE_NOT_FOUND); if(FileReadStruct(fp, Header) != class="kw">sizeof(Header)) class="kw">return MsgError(FAILED_READ, fp);
◍ BMP 像素流的翻转与通道补齐
解析位图头之后,先把宽度高度从文件头里取出来转成整型。若高度为负,说明图像无需垂直翻转,把 noFlip 置真并取绝对值;这一步决定了后面要不要做上下半图对调。
- 位图直接整块读入 data 数组,imgSize 就是 FileReadArray 返回的字节数。若 noFlip 为假,用三层 ArrayCopy 把第 c0 行和 (height-c0-1) 行互换,循环只跑 height/2 次就能完成镜像校正。
随后扫一遍像素,看有没有 alpha 通道非 0 的字节;全 0 则 noAlpha 保持真,统一把每个像素高 8 位或上 0xFF000000 补齐不透明。24 位图则按 byteWidth = (width*3+3) & ~3 做 4 字节对齐,逐行读入 tmp 再拼成 BGRA 写入 data,行序同样是倒着填的。 这套逻辑跑通后,FileClose 收尾并返回 true;若打不开 .bmp 文件则走 INVALID_HANDLE 分支。开 MT5 把这段塞进资源加载函数,换一张 32 位和一张 24 位图分别拖进去,能在 OBJ_BITMAP_LABEL 上肉眼验证翻转与透明差异。
width = (class="type">int)Header.imgWidth; height = (class="type">int)Header.imgHeight; if(noFlip = (height < class="num">0)) height = -height; if(Header.imgBitCount == class="num">32) { class="type">uint tmp[]; noAlpha = true; imgSize = FileReadArray(fp, data); if(!noFlip) for(class="type">int c0 = class="num">0; c0 < height / class="num">2; c0++) { ArrayCopy(tmp, data, class="num">0, width * c0, width); ArrayCopy(data, data, width * c0, width * (height - c0 - class="num">1), width); ArrayCopy(data, tmp, width * (height - c0 - class="num">1), class="num">0, width); } for(class="type">uint c0 = class="num">0; (c0 < imgSize && noAlpha); c0++) if(class="type">uchar(data[c0] >> class="num">24) != class="num">0) noAlpha = false; if(noAlpha) for(class="type">uint c0 = class="num">0; c0 < imgSize; c0++) data[c0] |= 0xFF000000; } else { class="type">int byteWidth; class="type">uchar tmp[]; byteWidth = width * class="num">3; byteWidth = (byteWidth + class="num">3) & ~class="num">3; if(ArrayResize(data, width * height) != -class="num">1) for(class="type">int c0 = class="num">0; c0 < height; c0++) { if(FileReadArray(fp, tmp, class="num">0, byteWidth) != byteWidth) class="kw">return MsgError(FAILED_READ, fp); else for(class="type">int j = class="num">0, k = class="num">0, p = width * (height - c0 - class="num">1); j < width; j++, k+=class="num">3, p++) data[p] = 0xFF000000 | (tmp[k+class="num">2] << class="num">16) | (tmp[k + class="num">1] << class="num">8) | tmp[k]; } } FileClose(fp); class="kw">return true; } if((fp = FileOpen(szFileName + ".bmp", FILE_READ | FILE_BIN)) == INVALID_HANDLE)
「Bitmap 背景图的透明度与自适应拉伸」
在 MT5 里用 OBJ_BITMAP_LABEL 做全屏背景,第一步是 Init 里把外部位图读进内存并控制透明度。cView 参数代表不透明度百分比,代码里先用三元运算把它夹在 0~100,再乘 2.55 除以 255.0 得到 0~1 的归一化 alpha 系数 dValue,这一步直接决定背景是纯遮罩还是半透叠加。 像素循环里对原图每个 ARGB 像素只改最高字节(Alpha 通道):m_BMP[i] = (uchar(double(m_BMP[i]>>24) * dValue) << 24) | m_BMP[i] & 0x00FFFFFF。也就是说 RGB 分量原样保留,仅按 dValue 缩放透明度,例如 cView=100 时 dValue=1.0 完全不透明,cView=50 时 alpha 减半。 Resize 函数负责图表窗口尺寸变化时的重绘适配。它从 ChartGetInteger 取 CHART_HEIGHT_IN_PIXELS 和 CHART_WIDTH_IN_PIXELS,算出横向缩放 fx、纵向缩放 fy,再把 m_Pixels 重新分配为 m_Height*m_Width 并初始化为 0x00FFFFFF(全透白),随后按原图行列映射填像素。外汇与贵金属图表挂这类对象属高风险界面改造,参数没调好可能挡住价格动作,建议先在模拟盘验证。 让小布替你跑这套:把 cView 从默认 100 改成 30,能直观看到 K 线在半透 logo 上的可读性问题,比纯读代码更快定位透明度拐点。
class="type">bool Init(class="kw">const class="type">class="kw">string szName, class="kw">const class="type">char cView = class="num">100, class="kw">const class="type">int iSub = class="num">0) { class="type">class="kw">double dValue = ((cView > class="num">100 ? class="num">100 : (cView < class="num">0 ? class="num">0 : cView)) * class="num">2.55) / class="num">255.0; m_Id = ChartID(); if(!LoadBitmap(szName, m_BMP, m_MemWidthBMP, m_MemHeightBMP)) class="kw">return false; Destroy(); m_Height = m_MemHeightBMP; m_Width = m_MemWidthBMP; if(ArrayResize(m_Pixels, (m_MemSizeArr = m_Height * m_Width)) < class="num">0) class="kw">return MsgError(FAILED_ALLOC); m_szRcName = "::" + szName + (class="type">class="kw">string)(GetTickCount64() + MathRand()); if(!ResourceCreate(m_szRcName, m_Pixels, m_Width, m_Height, class="num">0, class="num">0, class="num">0, COLOR_FORMAT_ARGB_NORMALIZE)) class="kw">return MsgError(FAILED_CREATE); if(!ObjectCreate(m_Id, (m_szObjName = szName), OBJ_BITMAP_LABEL, iSub, class="num">0, class="num">0)) class="kw">return MsgError(FAILED_CREATE); ObjectSetInteger(m_Id, m_szObjName, OBJPROP_XDISTANCE, class="num">0); ObjectSetInteger(m_Id, m_szObjName, OBJPROP_YDISTANCE, class="num">0); ObjectSetString(m_Id, m_szObjName, OBJPROP_BMPFILE, m_szRcName); ObjectSetInteger(m_Id, m_szObjName, OBJPROP_BACK, true); for(class="type">uint i = class="num">0; i < m_MemSizeArr; i++) m_BMP[i] = (class="type">uchar(class="type">class="kw">double(m_BMP[i] >> class="num">24) * dValue) << class="num">24) | m_BMP[i] & 0x00FFFFFF; class="kw">return true; } if(ResourceCreate(m_szRcName, m_Pixels, m_Width, m_Height, class="num">0, class="num">0, class="num">0, COLOR_FORMAT_ARGB_NORMALIZE)) ChartRedraw(); class="type">void Resize(class="type">void) { m_Height =(class="type">uint) ChartGetInteger(m_Id, CHART_HEIGHT_IN_PIXELS); m_Width = (class="type">uint) ChartGetInteger(m_Id, CHART_WIDTH_IN_PIXELS); class="type">class="kw">double fx = (m_Width * class="num">1.0) / m_MemWidthBMP; class="type">class="kw">double fy = (m_Height * class="num">1.0) / m_MemHeightBMP; class="type">uint pyi, pyf, pxi, pxf, tmp; ArrayResize(m_Pixels, m_Height * m_Width); ArrayInitialize(m_Pixels, 0x00FFFFFF); for (class="type">uint cy = class="num">0, y = class="num">0; cy < m_MemHeightBMP; cy++, y += m_MemWidthBMP) { pyf = (class="type">uint)(fy * cy) * m_Width;
位图缩放的像素填充逻辑
这段逻辑处理的是把一张小位图(m_BMP)按 fx、fy 比例放大写入目标像素缓冲(m_Pixels)的过程,常见于自定义指标里把离线算好的图形塞回图表资源。 外层先按纵向比例定位:tmp 算出上一行在缓冲里的起始偏移,pyf 是当前目标行。横向循环里 pxf 和 pxi 分别算目标列与上一列,先把当前源像素写进对应位置,再用内循环把中间因放大产生的空列全部填成同一源色,避免拉伸后出现竖条缝隙。 纵向补漏更粗暴:pyi 从上一行起始按 m_Width 步长往下走,直到小于 pyf,每一整行直接复制 tmp 那行的像素。这意味着放大 2 倍以上时,多行完全复制,肉眼看是块化但计算极省。 最后 ResourceCreate 用 COLOR_FORMAT_ARGB_NORMALIZE 把缓冲注册成图表资源,成功才 ChartRedraw。外汇与贵金属图表高频重绘时,这种写法可能降低 CPU 占用,但放大倍率过高会明显损失清晰度,属高风险环境下的视觉取舍。
tmp = pyi = (class="type">uint)(fy * (cy - class="num">1)) * m_Width; for (class="type">uint x = class="num">0; x < m_MemWidthBMP; x++) { pxf = (class="type">uint)(fx * x); pxi = (class="type">uint)(fx * (x - class="num">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 (class="type">uint x = class="num">0; x < m_Width; x++) m_Pixels[x + pyi] = m_Pixels[x + tmp]; } if (ResourceCreate(m_szRcName, m_Pixels, m_Width, m_Height, class="num">0, class="num">0, class="num">0, COLOR_FORMAT_ARGB_NORMALIZE)) ChartRedraw(); } for (pxi++; pxi < pxf; pxi++) m_Pixels[pxi + pyf] = m_BMP[x + y]; for (pyi += m_Width; pyi < pyf; pyi += m_Width) for (class="type">uint x = class="num">0; x < m_Width; x++) m_Pixels[x + pyi] = m_Pixels[x + tmp];
◍ EA里先挂壁纸才不挡图形
换背景图不用重新编译,这点对盯盘的人很实用:原图看腻了,直接在设置里挑张新的当图表底图就行,省得动代码。 但有个坑得记住——如果你把背景置换类塞进EA里用,它必须是Init例程里声明的第一件事。否则EA后面生成的画线、面板、按钮全压在壁纸上,图层顺序就乱了。 外汇和贵金属本身波动剧烈、杠杆高风险大,拿这套玩法做视觉辅助可以,别把它当成分析信号本身。挂好壁纸,图层顺序对了,你就能安安心心看盘几小时。