让图表更有趣:添加背景·进阶篇
📘

让图表更有趣:添加背景·进阶篇

第 2/2 篇

把 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。

MQL5 / C++
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 上肉眼验证翻转与透明差异。

MQL5 / C++
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 上的可读性问题,比纯读代码更快定位透明度拐点。

MQL5 / C++
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 占用,但放大倍率过高会明显损失清晰度,属高风险环境下的视觉取舍。

MQL5 / C++
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后面生成的画线、面板、按钮全压在壁纸上,图层顺序就乱了。 外汇和贵金属本身波动剧烈、杠杆高风险大,拿这套玩法做视觉辅助可以,别把它当成分析信号本身。挂好壁纸,图层顺序对了,你就能安安心心看盘几小时。

常见问题

BMP头按1字节对齐,C结构体默认会补 padding;用 #pragma pack(push,1) 包住结构体定义,读出来才不会偏。
BMP 存储从下往上、从左到右;逐行拷贝时把行序倒过来,并把 RGB 补成图表需要的 BGRA 四通道即可。
可以,小布能读取你当前品种页的背景图参数,提示透明度是否过高挡住K线,以及拉伸比是否变形。
透明度没写进 Alpha 通道或 EA 绘制顺序在指标之后;先 CreateBitmap 挂底图,再画指标层才不挡。
缩放时用最近邻填像素避免插值发虚,按图表宽高除原图算步长,逐格拷贝像素即可。