DirectX 教程(第一部分):绘制第一个三角形·进阶篇
🔺

DirectX 教程(第一部分):绘制第一个三角形·进阶篇

(2/3)· 从 3D 图形概念到 MQL 动作序列,拆解 MT5 隐藏例程后的真实绘制链路

含代码示例 第 2/3 篇

很多交易者想在 MT5 图表上画点自定义图形,一碰到 DirectX 就卡在初始化和结构填充上。MetaQuotes 把底层例程藏了,反而让人看不清三角形到底怎么跑到屏幕上的。这篇接着基础篇,直接进图形管道和 MQL 调用序列。

GPU 只认三角形和顶点属性

现代显卡的强项就是快速光栅化海量三角形。3D 建模软件里画的是矩形面,但到了图形 API 这一层,全部被拆成三角形——因为三点定义一个平面,硬件流水线只吃这套图元。 在 Direct3D 里,一个三角形必须给满三个顶点。顶点不只是空间坐标,还能附带颜色、纹理坐标和法线;光栅化时 XY 坐标必须落在 [-1,1],Z 在 [0,1],矩阵变换通常就为了把这个规范掉。 颜色本质是 RGB 三分量,对应人眼视锥细胞和屏幕子像素的结构。MQL5 里用 ColorToARGB 把 web 色转成带透明通道的 ARGB;32 位色下每个分量取 0–255(即 2^8-1),现今多数显示器就是 32 位色深。想验证?在 MT5 里调一下 ColorToARGB 看返回值的高字节是不是透明度。

◍ DirectX 显图的动作链怎么排

在 MQL5 里走 DirectX 渲染,不是调一个函数就出图,而是一条固定顺序的动作链。先 ObjectCreate 建位图标签,再 ResourceCreate 开动态资源,用 ObjectSetString 带 OBJPROP_BMPFILE 把资源和对象绑上,这一步漏了图表上什么都不会显示。 着色器侧要自己写 HLSL 顶点和像素着色器,用 #resource 引进来,按 DXVertexLayout 数组描述顶点格式。随后 DXContextCreate 建上下文,分别用 DXShaderCreate 配 DX_SHADER_VERTEX / DX_SHADER_PIXEL 建两种着色器,DXBufferCreate 配 DX_BUFFER_VERTEX(索引可选 DX_BUFFER_INDEX)开缓冲区。 真正下命令前还有几道绑定:DXShaderSetLayout 传顶点格式、DXPrimiveTopologySet 设拓扑、DXShaderSet 绑着色器、DXBufferSet 绑缓冲。清完深度缓冲(必要时清颜色缓冲)后,DXDraw 或 DXDrawIndexed 发渲染,DXContextGetColors 取回结果,再 ResourceCreate 更新资源,ChartRedraw 刷新图表。 收尾容易被忽略:DXRelease 释放、ResourceFree 删资源、ObjectDelete 删对象,不清理会吃显存。整条链 20 步左右,顺序错了 MT5 大概率黑屏或报错,建议开 MT5 按这个次序抄一遍验证。

「在 MT5 里搭一个 DirectX 画布类」

用 DirectX 在 MT5 做自定义渲染,核心是把「画布 — 设备 — 着色器 — 资源释放」封装进一个类。下面这个类把画布宽高、图像数组、图形上下文句柄和三个 DirectX 句柄(顶点着色器、像素着色器、顶点缓冲区)都收在私有成员里,初始化时若宽度或高度小于 1 会被强制设为 500 像素,顶点数组长度必须为 3,且每个顶点的 X/Y 须在 [-1,1]、Z 须为 0、W 须为 1,否则当场重置。 构造器只把四个 DirectX 句柄置 0,析构器直接调 Deinit() 释放;对外只暴露 Init() 和 Draw()。Init() 收顶点数组和可选宽高,校验完后依次调 InitCanvas() 与 InitDevice(),后者需要一张含 3 个 float4 顶点的数组作为输入,返回 bool 表示进度。 InitCanvas() 会建一个名为 DXTutorialCanvas 的位图标签对象,从左上角右移、下移各 100 像素,再绑一个叫 ::DXTutorialResource 的动态图形资源,数组用黑色(ARGB)填充。InitDevice() 才真正建图形上下文、编译两个着色器、建顶点缓冲区,并把图元类型设成三角形列表。 DirectX 不自动回收,Deinit() 里要手动 DXRelease 四个句柄,再用 ResourceFree 删动态资源、ObjectDelete 删标签对象。开 MT5 新建脚本把下面类贴进去,配一份 3 顶点数组就能跑通最薄的一层渲染管线。

MQL5 / C++
class DXTutorial
  {
class="kw">private:
   class="type">int                m_width;
   class="type">int                m_height;
   class="type">uint               m_image[];
   class="type">class="kw">string             m_canvas;
   class="type">class="kw">string             m_resource;
   class="type">int                m_dx_context;
   class="type">int                m_dx_vertex_shader;
   class="type">int                m_dx_pixel_shader;
   class="type">int                m_dx_buffer;
   class="type">bool               InitCanvas();
   class="type">bool               InitDevice(float4 &vertex[]);
   class="type">void               Deinit();
class="kw">public:
   class="type">void               DXTutorial() { m_dx_context = class="num">0; m_dx_vertex_shader = class="num">0; m_dx_pixel_shader = class="num">0; m_dx_buffer = class="num">0; }
   class="type">void              ~DXTutorial() { Deinit(); }
   class="type">bool               Init(float4 &vertex[], class="type">int width, class="type">int height);
   class="type">bool               Draw();
  };

三角形顶点与画布的初始化校验

在 MT5 里用 DirectX 画一个基础三角形,第一步是把顶点数据塞进 float4 结构数组。下面这段定义了 3 个顶点,x/y 落在 [-1,1] 的裁剪空间,z 固定为 0,w 统一为 1,正好构成一个居中等边三角形雏形。 Init 函数先兜底画布尺寸:width 或 height 传进来 ≤0 时,强制回退成 500×500 并打印警告,避免后续创建位图标签时因零面积崩掉。顶点数组长度必须严格等于 3,否则直接返回 false,因为三角形就靠这 3 个点。 循环里逐点修正:w 不是 1 就改成 1.0f,z 不是 0 就改成 0.0f,并各自打印警告;一旦 x 或 y 绝对值超过 1,立即报错返回 false——裁剪空间坐标越界 GPU 不会给你面子。 InitCanvas 负责挂 OBJ_BITMAP_LABEL 对象,名字写死成 DXTutorialCanvas,资源路径指向 ::DXTutorialResource。面积用 m_width*m_height 算,若 ObjectCreate 返回失败就打印错误并退出。开 MT5 新建 EA 把这段抄进去,改改顶点坐标就能肉眼验证画布有没有起来。

MQL5 / C++
class="kw">struct float4
  {
   class="type">float               x;
   class="type">float               y;
   class="type">float               z;
   class="type">float               w;
  };
float4 vertex[class="num">3] = {{-class="num">0.5f, -class="num">0.5f, class="num">0.0f, class="num">1.0f}, {class="num">0.0f, class="num">0.5f, class="num">0.0f, class="num">1.0f}, {class="num">0.5f, -class="num">0.5f, class="num">0.0f, class="num">1.0f}};
class="type">bool DXTutorial::Init(float4 &vertex[], class="type">int width = class="num">500, class="type">int height = class="num">500)
  {
   if(width <= class="num">0)
     {
      m_width = class="num">500;
      Print("Warning: width changed to class="num">500");
     }
   else
     {
      m_width = width;
     }
   if(height <= class="num">0)
     {
      m_height = class="num">500;
      Print("Warning: height changed to class="num">500");
     }
   else
     {
      m_height = height;
     }
   if(ArraySize(vertex) != class="num">3)
     {
      Print("Error: class="num">3 vertex are needed for a triangle");
      class="kw">return(false);
     }
   for(class="type">int i = class="num">0; i < class="num">3; i++)
     {
      if(vertex[i].w != class="num">1)
        {
         vertex[i].w = class="num">1.0f;
         Print("Warning: vertex.w changed to class="num">1");
        }
      if(vertex[i].z != class="num">0)
        {
         vertex[i].z = class="num">0.0f;
         Print("Warning: vertex.z changed to class="num">0");
        }
      if(fabs(vertex[i].x) > class="num">1 || fabs(vertex[i].y) > class="num">1)
        {
         Print("Error: vertex coordinates must be in the range [-class="num">1;class="num">1]");
         class="kw">return(false);
        }
     }
   ResetLastError();
   if(!InitCanvas())
     {
      class="kw">return(false);
     }
   if(!InitDevice(vertex))
     {
      class="kw">return(false);
     }
   class="kw">return(true);
  }
class="type">bool DXTutorial::InitCanvas()
  {
   m_canvas = "DXTutorialCanvas";
   m_resource = "::DXTutorialResource";
   class="type">int area = m_width * m_height;
   if(!ObjectCreate(class="num">0, m_canvas, OBJ_BITMAP_LABEL, class="num">0, class="num">0, class="num">0))
     {
      Print("Error: failed to create an object to draw");
      class="kw">return(false);
     }

◍ 把画布与顶点缓冲真正挂上显卡

在 MT5 里用 DirectX 画自定义图形,第一步是把 OBJ_BITMAP_LABEL 对象摆到图表坐标 (100,100) 的位置,再把一块 ARGB 像素数组绑成位图资源。下面这段把对象位移、数组尺寸、资源创建串成一条链,任何一步返回 false 都会中断初始化。 if(!ObjectSetInteger(0, m_canvas, OBJPROP_XDISTANCE, 100)) { Print("Warning: failed to move the object horizontally"); } if(!ObjectSetInteger(0, m_canvas, OBJPROP_YDISTANCE, 100)) { Print("Warning: failed to move the object vertically"); } if(ArrayResize(m_image, area) != area) { Print("Error: failed to resize the array for the graphical resource"); return(false); } if(ArrayInitialize(m_image, ColorToARGB(clrBlack)) != area) { Print("Warning: failed to initialize array for graphical resource"); } if(!ResourceCreate(m_resource, m_image, m_width, m_height, 0, 0, m_width, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Error: failed to create a resource to draw"); return(false); } if(!ObjectSetString(0, m_canvas, OBJPROP_BMPFILE, m_resource)) { Print("Error: failed to bind resource to object"); return(false); } return(true); } m_canvas = "DXTutorialCanvas"; m_resource = "::DXTutorialResource"; int area = m_width * m_height; ObjectCreate(0, m_canvas, OBJ_BITMAP_LABEL, 0, 0, 0) ObjectSetInteger(0, m_canvas, OBJPROP_XDISTANCE, 100) ObjectSetInteger(0, m_canvas, OBJPROP_YDISTANCE, 100) ArrayResize(m_image, area) ArrayInitialize(m_image, ColorToARGB(clrBlack)) ResourceCreate(m_resource, m_image, m_width, m_height, 0, 0, m_width, COLOR_FORMAT_ARGB_NORMALIZE) ObjectSetString(0, m_canvas, OBJPROP_BMPFILE, m_resource) 逐行拆一下:前两个 if 把画布标签推到距图表左上角各 100 像素处,失败仅打印警告不中断;ArrayResize 按 area=m_width*m_height 开像素缓冲,尺寸不符直接 return(false);ArrayInitialize 填黑底,失败也只是警告。ResourceCreate 用 COLOR_FORMAT_ARGB_NORMALIZE 把数组注册成名称为 ::DXTutorialResource 的位图资源,最后 ObjectSetString 把资源和标签绑定,任一步失败都退出。 顶点布局与上下文则要另起炉灶。InitDevice 里先声明只有 POSITION 语义、用 R32G32B32A32_FLOAT 的单属性布局,再 DXContextCreate 拿上下文句柄,无效就带 GetLastError 退场;接着编译顶点着色器 "VShader",若句柄无效除了报错还会把 shader_error 文本打出来,方便你定位 HLSL 拼写。外汇与贵金属图表叠加这类自绘层属高风险试验,上下文创建失败往往意味着显卡特性不支持,先在模拟环境跑通再上真仓。

MQL5 / C++
if(!ObjectSetInteger(class="num">0, m_canvas, OBJPROP_XDISTANCE, class="num">100))
  {
    Print("Warning: failed to move the object horizontally");
  }
if(!ObjectSetInteger(class="num">0, m_canvas, OBJPROP_YDISTANCE, class="num">100))
  {
    Print("Warning: failed to move the object vertically");
  }
if(ArrayResize(m_image, area) != area)
  {
    Print("Error: failed to resize the array for the graphical resource");
    class="kw">return(false);
  }
if(ArrayInitialize(m_image, ColorToARGB(clrBlack)) != area)
  {
    Print("Warning: failed to initialize array for graphical resource");
  }
if(!ResourceCreate(m_resource, m_image, m_width, m_height, class="num">0, class="num">0, m_width, COLOR_FORMAT_ARGB_NORMALIZE))
  {
    Print("Error: failed to create a resource to draw");
    class="kw">return(false);
  }
if(!ObjectSetString(class="num">0, m_canvas, OBJPROP_BMPFILE, m_resource))
  {
    Print("Error: failed to bind resource to object");
    class="kw">return(false);
  }
class="kw">return(true);
}
m_canvas = "DXTutorialCanvas";
m_resource = "::DXTutorialResource";
class="type">int area = m_width * m_height;
ObjectCreate(class="num">0, m_canvas, OBJ_BITMAP_LABEL, class="num">0, class="num">0, class="num">0)
ObjectSetInteger(class="num">0, m_canvas, OBJPROP_XDISTANCE, class="num">100)
ObjectSetInteger(class="num">0, m_canvas, OBJPROP_YDISTANCE, class="num">100)
ArrayResize(m_image, area)
ArrayInitialize(m_image, ColorToARGB(clrBlack))
ResourceCreate(m_resource, m_image, m_width, m_height, class="num">0, class="num">0, m_width, COLOR_FORMAT_ARGB_NORMALIZE)
ObjectSetString(class="num">0, m_canvas, OBJPROP_BMPFILE, m_resource)
class="type">bool DXTutorial::InitDevice(float4 &vertex[])
  {
   DXVertexLayout layout[class="num">1] = {{"POSITION", class="num">0, DX_FORMAT_R32G32B32A32_FLOAT }};
   class="type">class="kw">string shader_error = "";
   m_dx_context = DXContextCreate(m_width, m_height);
   if(m_dx_context == INVALID_HANDLE)
     {
       Print("Error: failed to create graphics context: ", GetLastError());
       class="kw">return(false);
     }
   m_dx_vertex_shader = DXShaderCreate(m_dx_context, DX_SHADER_VERTEX, shader, "VShader", shader_error);
   if(m_dx_vertex_shader == INVALID_HANDLE)
     {
       Print("Error: failed to create vertex shader: ", GetLastError());
       Print("Shader compilation error: ", shader_error);
       class="kw">return(false);
     }
把重复劳动交给小布
这些 DirectX 初始化与资源释放的样板代码,小布盯盘的 AIGC 已内置常用模板,打开对应品种页即可调用,你专注着色器逻辑和图元设计。

常见问题

基础图元可用内置管道,但自定义颜色与变换通常要写简单 HLSL,本篇实践节会给出顶点着色器示例。
MetaQuotes 已封装大部分,MQL 侧只需关注画布创建与资源释放,底层设备由终端管理。
列表每三个顶点独立成面,条带相邻顶点复用,顶点数更少但顺序约束更强,按几何需求选。
可以,小布内置的代码片段库覆盖画布初始化与释放资源结构,能减少手写 C++ 式结构的出错概率。
即使只画 2D 三角形,管道仍按 3D 顶点处理,输入布局错一位颜色或位置就会偏移甚至不显示。