字体和文本输出到图形资源

除了在图形资源的数组中渲染单个像素外,我们还可以使用内置函数来显示文本。这些函数用于更改当前字体及其特性 (TextSetFont),获取给定字符串可以内切的矩形的尺寸 (TextGetSize),以及直接将标题插入到生成的图像 (TextOut) 中。

bool TextSetFont(const string name, int size, uint flags, int orientation = 0)

该函数为后续使用 TextOut 函数(见下文)在图像缓冲区中绘制文本设置字体及其特性。name 参数可以包含内置 Windows 字体的名称或通过资源指令连接的 ttf 字体文件(TrueType 字体)(如果名称以 "::" 开头)。

大小 (size) 可以用点(一种印刷计量单位)或像素(屏幕点)指定。正值表示计量单位是像素,负值以十分之一点为单位进行测量。以像素为单位的高度对于不同用户来说看起来会有所不同,具体取决于其显示器的技术能力和设置。以点为单位的高度对于每个人来说大致(“凭肉眼判断”)相同。

印刷点是一种物理长度单位,传统上等于 1/72 英寸。因此,1 点等于 0.352778 毫米。屏幕上的像素是一种虚拟的长度度量。其物理尺寸取决于屏幕的硬件分辨率。例如,屏幕密度为 96 DPI(每英寸点数)时,1 像素将占据 0.264583 毫米或 0.75 点。然而,大多数现代显示器具有更高的 DPI 值,因此像素更小。因此,包括 Windows 在内的操作系统长期以来都有增加界面元素可见比例的设置。因此,如果你以点为单位指定大小(负值),则文本的像素大小将取决于显示器和操作系统中的缩放设置(例如,“标准”100%,“中等”125% 或“大”150%)。
 
放大时,系统会人为地放大显示的像素。这相当于以像素为单位减小屏幕尺寸,系统会应用有效 DPI 来实现相同的物理尺寸。如果启用了缩放,则报告给程序(包括终端,然后是 MQL 程序)的是有效 DPI。如有必要,可以从 TERMINAL_SCREEN_DPI 属性中找出屏幕的 DPI(请参见 屏幕规格)。然而实际上,通过以点为单位设置字体大小,我们无需根据 DPI 重新计算其大小,因为系统会为我们完成此操作。

默认字体是 Arial,默认大小是 -120 (12 pt)。控件,特别是图表上的内置对象,也以点为单位操作字体大小。例如,如果在 MQL 程序中,你希望绘制与 OBJ_LABEL 对象中文本大小相同的文本,该对象的大小为 10 点,则应使用等于 -100 的参数 size

flags 参数用于设置描述字体样式的标志组合。该组合是使用按位或运算符 ('|') 构成的位掩码。标志分为两组:样式标志和粗细标志。

下表列出了样式标志。它们可以混合使用。

标志

说明

FONT_ITALIC

斜体

FONT_UNDERLINE

下划线

FONT_STRIKEOUT

删除线

粗体标志具有与其对应的相对权重(用于比较预期效果)。

标志

说明

FW_DONTCARE

0(将应用系统默认值)

FW_THIN

100

FW_EXTRALIGHT, FW_ULTRALIGHT

200

FW_LIGHT

300

FW_NORMAL、FW_REGULAR

400

FW_MEDIUM

500

FW_SEMIBOLD、FW_DEMIBOLD

600

FW_BOLD

700

FW_EXTRABOLD、FW_ULTRABOLD

800

FW_HEAVY、FW_BLACK

900

在标志组合中仅使用这些值中的一个。

orientation 参数指定文本相对于水平线的角度,单位为十分之一度。例如,orientation = 0 表示正常文本输出,而 orientation = 450 将导致 45 度倾斜(逆时针)。

请注意,一次 TextSetFont 调用中所做的设置将影响所有后续的 TextOut 调用,直到它们被更改为止。

如果成功,函数返回 true;如果出现问题(例如,未找到字体),则返回 false

在描述完所有这三个函数后,我们将考虑一个使用此函数的示例,以及另外两个函数的示例。

bool TextGetSize(const string text, uint &width, uint &height)

该函数返回当前字体设置下(可以是默认字体或先前调用 TextSetFont 时指定的字体)文本行的宽度和高度。

text 参数传递一个需要计算像素长度和宽度的字符串。维度值由函数根据 widthheight 参数中的引用写入。

需要注意的是,调用 TextSetFont 时由 orientation 参数指定的显示文本的旋转(倾斜)不会以任何方式影响大小计算。换句话,如果文本应该旋转 45 度,那么 MQL 程序本身必须计算文本可以容纳的最小正方形。TextGetSize 函数计算标准(水平)位置的文本大小。

bool TextOut(const string text, int x, int y, uint anchor, uint &data[], uint width, uint height, uint color, ENUM_COLOR_FORMAT color_format)

该函数在图形缓冲区中的指定坐标处绘制文本,同时考虑颜色、格式和先前的设置(字体、样式和方向)。

文本在 text 参数中传递,并且必须是单行形式。

以像素为单位指定的 xy 坐标定义了图形缓冲区中显示文本的点。生成的题字的哪个位置将位于点 (x, y) 取决于 anchor 参数中的绑定方法(见下文)。

缓冲区由 data 数组表示,尽管该数组是一维的,但它存储了一个维度为 width x height 个点的二维“画布”。该数组可以从 ResourceReadImage 函数获取,或者由 MQL 程序分配。在完成所有编辑操作(包括文本输出)后,应该基于此缓冲区创建一个新资源或将其应用于已有的资源。在这两种情况下,都应该调用 ResourceCreate

文本的颜色和颜色的处理方式由参数 colorcolor_format 设置(请参见 ENUM_COLOR_FORMAT)。请注意,用于颜色的类型是 uint,即,要传递 color,应使用 ColorToARGB 进行转换。

由 anchor 参数指定的锚定方法是两个文本定位标志的组合:垂直和水平。

水平文本位置标志有:

  • TA_LEFT – 锚定到边界框的左侧
  • TA_CENTER – 锚定到矩形左右两侧的中间
  • TA_RIGHT – 锚定到边界框的右侧

垂直文本位置标志有:

  • TA_TOP – 锚定到边界框的顶部
  • TA_VCENTER – 锚定到矩形顶部和底部之间的中间
  • TA_BOTTOM – 锚定到边界框的底部

总共有 9 种有效的标志组合来描述锚定方法。

输出文本相对于锚点的位置

输出文本相对于锚点的位置

在这里,图片的中心包含一个在生成的图像中坐标为 (x, y) 的刻意放大的点。根据标志,文本相对于此点出现在指定位置(文本内容对应于应用的锚定方法)。

为便于参考,所有题字均以标准水平位置制作。但请注意,也可以对其中任何一个应用角度 (orientation),然后相应的题字将围绕该点旋转。在此图像中,仅旋转了在两个维度上都居中的标签。

这些标志不应与文本对齐混淆。边界框的大小始终适合文本,其相对于锚点的位置在某种意义上与标志名称相反。

我们看一些使用三个函数的示例。

首先,让我们检查设置字体粗细和样式的最简单选项。ResourceText.mq5 脚本允许在输入变量中选择字体名称、其大小以及背景和文本的颜色。标签将在图表上显示指定的秒数。

input string Font = "Arial";             // Font Name
input int    Size = -240;                // Size
input color  Color = clrBlue;            // Font Color
input color  Background = clrNONE;       // Background Color
input uint   Seconds = 10;               // Demo Time (seconds)

每个粗细等级的名称将显示在标签文本中,因此为了简化过程(通过使用 EnumToString),声明了 ENUM_FONT_WEIGHTS 枚举。

enum ENUM_FONT_WEIGHTS
{
   _DONTCARE = FW_DONTCARE,
   _THIN = FW_THIN,
   _EXTRALIGHT = FW_EXTRALIGHT,
   _LIGHT = FW_LIGHT,
   _NORMAL = FW_NORMAL,
   _MEDIUM = FW_MEDIUM,
   _SEMIBOLD = FW_SEMIBOLD,
   _BOLD = FW_BOLD,
   _EXTRABOLD = FW_EXTRABOLD,
   _HEAVY = FW_HEAVY,
};
 
const int nw = 10; // number of different weights

题注标志收集在 rendering 数组中,并从中选择随机组合。

   const uint rendering[] =
   {
      FONT_ITALIC,
      FONT_UNDERLINE,
      FONT_STRIKEOUT
   };
   const int nr = sizeof(rendering) / sizeof(uint);

为了在一定范围内获得随机数,有一个辅助函数 Random

int Random(const int limit)
{
   return rand() % limit;
}

在脚本的主函数中,我们找到图表的大小并创建一个跨越整个空间的 OBJ_BITMAP_LABEL 对象。

void OnStart()
{
   ...
   const string name = "FONT";
   const int w = (int)ChartGetInteger(0CHART_WIDTH_IN_PIXELS);
   const int h = (int)ChartGetInteger(0CHART_HEIGHT_IN_PIXELS);
   
   // object for a resource with a picture filling the whole window
   ObjectCreate(0nameOBJ_BITMAP_LABEL000);
   ObjectSetInteger(0nameOBJPROP_XSIZEw);
   ObjectSetInteger(0nameOBJPROP_YSIZEh);
   ...

接下来,我们为图像缓冲区分配内存,用指定的背景颜色填充它(或默认情况下保持透明),基于该缓冲区创建一个资源,并将其绑定到对象。

   uint data[];
   ArrayResize(dataw * h);
   ArrayInitialize(dataBackground == clrNONE ? 0 : ColorToARGB(Background));
   ResourceCreate(namedatawh00wCOLOR_FORMAT_ARGB_RAW);
   ObjectSetString(0nameOBJPROP_BMPFILE"::" + name);
   ...

以防万一,请注意,除非对象应该在两种状态之间切换,否则我们可以在 ObjectSetString 调用中设置 OBJPROP_BMPFILE 属性而无需修饰符(0 或 1)。

所有字体粗细都列在 weights 数组中。

   const uint weights[] =
   {
      FW_DONTCARE,
      FW_THIN,
      FW_EXTRALIGHT// FW_ULTRALIGHT,
      FW_LIGHT,
      FW_NORMAL,     // FW_REGULAR,
      FW_MEDIUM,
      FW_SEMIBOLD,   // FW_DEMIBOLD,
      FW_BOLD,
      FW_EXTRABOLD,  // FW_ULTRABOLD,
      FW_HEAVY,      // FW_BLACK
   };
   const int nw = sizeof(weights) / sizeof(uint);

在循环中,我们依次使用 TextSetFont 为每行设置下一个粗细等级,预先选择一个随机样式。使用 TextOut 在缓冲区中绘制字体描述,包括其名称和粗细。

   const int step = h / (nw + 2);
   int cursor = 0;    // Y coordinate of the current "text line"
   
   for(int weight = 0weight < nw; ++weight)
   {
      // apply random style
      const int r = Random(8);
      uint render = 0;
      for(int j = 0j < 3; ++j)
      {
         if((bool)(r & (1 << j))) render |= rendering[j];
      }
      TextSetFont(FontSizeweights[weight] | render);
      
      // generate font description
      const string text = Font + EnumToString((ENUM_FONT_WEIGHTS)weights[weight]);
      
      // draw text on a separate "line"
      cursor += step;
      TextOut(textw / 2cursorTA_CENTER | TA_TOPdatawh,
         ColorToARGB(Color), COLOR_FORMAT_ARGB_RAW);
   }
   ...

现在更新资源和图表。

   ResourceCreate(namedatawh00wCOLOR_FORMAT_ARGB_RAW);
   ChartRedraw();
   ...

用户可以提前停止演示。

   const uint timeout = GetTickCount() + Seconds * 1000;
   while(!IsStopped() && GetTickCount() < timeout)
   {
      Sleep(1000);
   }

最后,脚本删除资源和对象。

   ObjectDelete(0name);
   ResourceFree("::" + name);
}

脚本的结果如下图所示。

以不同粗细和样式绘制文本

以不同粗细和样式绘制文本

ResourceFont.mq5 的第二个示例中,我们将通过包含自定义字体作为资源并使用 90 度增量的文本旋转来使任务更加困难。

字体文件位于脚本旁边。

#resource "a_LCDNova3DCmObl.ttf"

可以在输入参数中更改消息。

input string Message = "Hello world!";   // Message

这一次,OBJ_BITMAP_LABEL 不会占据整个窗口,因此在水平和垂直方向上都居中。

void OnStart()
{
   const string name = "FONT";
   const int w = (int)ChartGetInteger(0CHART_WIDTH_IN_PIXELS);
   const int h = (int)ChartGetInteger(0CHART_HEIGHT_IN_PIXELS);
   
   // object for a resource with a picture
   ObjectCreate(0nameOBJ_BITMAP_LABEL000);
   ObjectSetInteger(0nameOBJPROP_XDISTANCEw / 2);
   ObjectSetInteger(0nameOBJPROP_YDISTANCEh / 2);
   ObjectSetInteger(0nameOBJPROP_ANCHORANCHOR_CENTER);
   ...

首先,分配最小尺寸的缓冲区,只是为了完成资源创建。稍后我们将扩展它以适应题字的尺寸,为此保留了变量 widthheight

   uint data[], widthheight;
   ArrayResize(data1);
   ResourceCreate(namedata11001COLOR_FORMAT_ARGB_RAW);
   ObjectSetString(0nameOBJPROP_BMPFILE"::" + name);
   ...

在测试时间倒计时的循环中,我们需要更改题字的方向,为此有 angle 变量(度数将在其中滚动)。方向将每秒更改一次,计数在 remain 变量中。

   const uint timeout = GetTickCount() + Seconds * 1000;
   int angle = 0;
   int remain = 10;
   ...

在循环中,我们不断更改文本的旋转,并在文本本身中显示秒数的倒计时计数器。对于每个新的题字,使用 TextGetSize 计算其大小,并基于此重新分配缓冲区。

   while(!IsStopped() && GetTickCount() < timeout)
   {
      // apply new angle
      TextSetFont("::a_LCDNova3DCmObl.ttf", -2400angle * 10);
      
      // form the text
      const string text = Message + " (" + (string)remain-- + ")";
      
      // get the text size, allocate the array
      TextGetSize(textwidthheight);
      ArrayResize(datawidth * height);
      ArrayInitialize(data0);            // transparency
      
      // for vertical orientation, swap sizes
      if((bool)(angle / 90 & 1))
      {
         const uint t = width;
         width = height;
         height = t;
      }
      
      // adjust the size of the object
      ObjectSetInteger(0nameOBJPROP_XSIZEwidth);
      ObjectSetInteger(0nameOBJPROP_YSIZEheight);
      
      // draw text
      TextOut(textwidth / 2height / 2TA_CENTER | TA_VCENTERdatawidthheight,
         ColorToARGB(clrBlue), COLOR_FORMAT_ARGB_RAW);
      
      // update resource and chart
      ResourceCreate(namedatawidthheight00widthCOLOR_FORMAT_ARGB_RAW);
      ChartRedraw();
      
      // change angle
      angle += 90;
      
      Sleep(100);
   }
   ...

请注意,如果文本是垂直的,则需要交换尺寸。更一般地,对于旋转到任意角度的文本,需要更多的数学运算才能获得适合整个文本的缓冲区大小。

最后,我们还删除对象和资源。

   ObjectDelete(0name);
   ResourceFree("::" + name);
}

脚本执行的某个时刻如下图所示。

带有自定义字体的题字

带有自定义字体的题字

作为最后一个示例,我们看一下 ResourceTextAnchOrientation.mq5 脚本,它显示了文本的各种旋转和锚点。

该脚本使用指定的字体生成指定数量的标签 (ExampleCount)。

input string Font = "Arial";             // Font Name
input int    Size = -150;                // Size
input int    ExampleCount = 11;          // Number of examples

锚点和旋转是随机选择的。

为了在标签中指定锚点的名称,声明了包含所有有效选项的 ENUM_TEXT_ANCHOR 枚举。因此,我们可以简单地对任何随机选择的元素调用 EnumToString

enum ENUM_TEXT_ANCHOR
{
   LEFT_TOP = TA_LEFT | TA_TOP,
   LEFT_VCENTER = TA_LEFT | TA_VCENTER,
   LEFT_BOTTOM = TA_LEFT | TA_BOTTOM,
   CENTER_TOP = TA_CENTER | TA_TOP,
   CENTER_VCENTER = TA_CENTER | TA_VCENTER,
   CENTER_BOTTOM = TA_CENTER | TA_BOTTOM,
   RIGHT_TOP = TA_RIGHT | TA_TOP,
   RIGHT_VCENTER = TA_RIGHT | TA_VCENTER,
   RIGHT_BOTTOM = TA_RIGHT | TA_BOTTOM,
};

这些新常量的数组在 OnStart 处理程序中声明。

void OnStart()
{
   const ENUM_TEXT_ANCHOR anchors[] =
   {
      LEFT_TOP,
      LEFT_VCENTER,
      LEFT_BOTTOM,
      CENTER_TOP,
      CENTER_VCENTER,
      CENTER_BOTTOM,
      RIGHT_TOP,
      RIGHT_VCENTER,
      RIGHT_BOTTOM,
   };
   const int na = sizeof(anchors) / sizeof(uint);
   ...

初始对象和资源创建类似于 ResourceText.mq5 中的示例,因此我们在此省略它们。最有趣的事情发生在循环中。

   for(int i = 0i < ExampleCount; ++i)
   {
      // apply a random angle
      const int angle = Random(360);
      TextSetFont(FontSize0angle * 10);
      
      // take random coordinates and an anchor point
      const ENUM_TEXT_ANCHOR anchor = anchors[Random(na)];
      const int x = Random(w / 2) + w / 4;
      const int y = Random(h / 2) + h / 4;
      const color clr = ColorMix::HSVtoRGB(angle);
      
     // draw a circle directly in that place of the image,
     // where the anchor point goes
      TextOut(ShortToString(0x2022), xyTA_CENTER | TA_VCENTERdatawh,
         ColorToARGB(clr), COLOR_FORMAT_ARGB_NORMALIZE);
      
      // form the text describing the anchor type and angle
      const string text =  EnumToString(anchor) +
         "(" + (string)angle + CharToString(0xB0) + ")";
   
      // draw text
      TextOut(textxyanchordatawh,
         ColorToARGB(clr), COLOR_FORMAT_ARGB_NORMALIZE);
   }
   ...

剩下的只是更新图片和图表,然后等待用户的命令并释放资源。

   ResourceCreate(namedatawh00wCOLOR_FORMAT_ARGB_NORMALIZE);
   ChartRedraw();
   
   const uint timeout = GetTickCount() + Seconds * 1000;
   while(!IsStopped() && GetTickCount() < timeout)
   {
      Sleep(1000);
   }
   
   ObjectDelete(0name);
   ResourceFree("::" + name);
}

结果如下。

具有随机坐标、锚点和角度的文本输出

具有随机坐标、锚点和角度的文本输出

此外,为了独立学习,本书提供了一个玩具图形编辑器 SimpleDrawing.mq5。它被设计为一个无缓冲区的指标,并在其工作中使用先前考虑的形状类(参见 ResourceShapesDraw.mq5)。它们几乎没有改动地放在头文件 ShapesDrawing.mqh 中。以前,形状是由脚本随机生成的。现在用户可以在图表上选择并绘制它们。为此,根据注册的形状类的数量,实现了一个带有调色板和按钮栏的界面。该界面由 SimpleDrawing 类 (SimpleDrawing.mqh) 实现。

简单图形编辑器

简单图形编辑器

面板和调色板可以沿图表的任何边界放置,展示了旋转标签的能力。

通过按下面板中的按钮来选择要绘制的下一个形状:按钮会“粘”在按下状态,其背景颜色表示选定的绘图颜色。要更改颜色,请点击调色板上的任意位置。

当在面板中选择其中一种形状类型时(其中一个按钮处于“活动”状态),点击绘图区域(图表的其余部分,用阴影表示)会在该位置绘制预定义大小的形状。此时,按钮“关闭”。在此状态下,当所有按钮都处于非活动状态时,你可以使用鼠标在工作区中移动形状。如果我们按住 Ctrl,则形状会调整大小而不是移动。“热点”位于每个形状的中心(敏感区域的大小由源代码中的宏设置,对于非常高 DPI 的显示器可能需要增加)。

请注意,编辑器在生成的资源的名称中包含绘图 ID (ChartID)。这允许在多个图表上并行运行编辑器。