确定对象宽度和高度

某些对象类型支持以像素为单位设置尺寸。这些对象包括 OBJ_BUTTON、OBJ_CHART、OBJ_BITMAP、OBJ_BITMAP_LABEL、OBJ_EDIT 和 OBJ_RECTANGLE_LABEL。此外,OBJ_LABEL 对象仅支持读取(不支持设置)尺寸,因为标签会根据所含文本自动调整大小。如果尝试访问其他类型对象的特性,将导致 OBJECT_WRONG_PROPERTY (4203) 错误。

标识符

说明

OBJPROP_XSIZE

对象在 X 轴方向的宽度,以像素为单位

OBJPROP_YSIZE

对象在 Y 轴方向的高度,以像素为单位

这两个尺寸均为整数,需通过 ObjectGetInteger/ObjectSetInteger函数处理。

对于 OBJ_BITMAP 和 OBJ_BITMAP_LABEL 对象,执行特殊的尺寸处理。

未指定图像时,这些对象允许设置任意尺寸。此时,对象以透明方式绘制(如果未通过设置颜色为 clrNone隐藏边框,则仅边框可见),但仍会接收所有事件,尤其是鼠标移动事件(如果有文本描述则显示在工具提示中)和按钮点击对象事件。

分配图像后,图像默认采用对象的高度和宽度。但 MQL 程序可设置更小尺寸并选择显示图像片段,详情请参阅 帧设置章节。如果尝试将高度或宽度设置为大于图像尺寸,图像将停止显示,且对象尺寸保持不变。

作为示例,我们将开发一个 在对象上定义锚点章节中ObjectAnchorLabel.mq5脚本的改进版本。在该章节中,我们实现了文本标签在窗口内的移动,并在其接触窗口边框时反转方向,但当时我们仅考虑了锚点位置。因此,根据锚点在对象上的位置不同,可能出现标签几乎完全移出窗口的情况。例如,如果锚点位于对象右侧,向左移动时在锚点接触边缘前,几乎全部文本可能就已超出窗口左边框。

在新脚本 ObjectSizeLabel.mq5中,我们将考虑对象尺寸,当对象任一边缘接触窗口边框时立即改变移动方向。

为确保该模式正确运行,需考虑以下因素,在将窗口的任意角点用作对象锚点的坐标参考中心时,会决定 X 轴和 Y 轴的方向特性。例如,如果用户在 ENUM_BASE_CORNER Corner 输入变量中选择左上角,则 X 轴方向为从左向右递增,Y 轴方向为从上向下递增。如果将参考中心设为右下角,则 X 轴方向为从右向左递增,Y 轴方向为从下向上递增。

窗口锚点角点与对象锚点位置的不同组合,需要对对象边缘与窗口边框的距离进行不同调整。具体而言,当选择窗口右侧某个角点和对象右侧的某个锚点时,则无需调整窗口右侧边框的距离,而在相反的左侧,则必须考虑对象的宽度(以确保其尺寸不会超出窗口左侧)。

关于对象尺寸调整的这一规则可归纳为以下节点:

  • 在与锚点角点相邻的窗口边框处,当锚点位于对象远端(相对于该角点),则需进行调整;
  • 在与锚点角点相邻的窗口边框处,当锚点位于对象近端(相对于该角点),则需进行调整。

换言之,如果角点名称(来自 ENUM_BASE_CORNER 元素)与锚点名称(来自 ENUM_ANCHOR_POINT 元素)包含相同词(例如 RIGHT),则需在窗口远端(即远离所选角点的一侧)进行调整。如果 ENUM_BASE_CORNER 与 ENUM_ANCHOR_POINT 组合中的方向相反(例如 LEFT 与 RIGHT),则需在窗口近端进行调整。这些规则同样适用于水平轴和垂直轴。

此外还需注意,锚点可能位于对象任意一条边的中点位置。此时在垂直方向上,需要设置与窗口边框的缩进,缩进值应为对象对应尺寸的一半。

特殊情况是锚点位于对象中心的情况。在任意方向上均需预留等于对象一半尺寸的距离余量。

上述逻辑通过名为GetMargins的特殊函数实现。该函数的输入参数包括所选角点、锚点位置以及对象尺寸(dxdy)。该函数返回一个包含 4 个字段的结构,这些字段分别表示从锚点向窗口近端和远端边框方向应预留的额外缩进量,以确保对象不会超出可视区域。缩进量根据对象自身的尺寸和相对位置进行预留。

struct Margins
{
   int nearX// X increment between the object point and the window border adjacent to the corner
   int nearY// Y increment between the object point and the window border adjacent to the corner
   int farX;  // X increment between the object's point and the opposite corner of the window border
   int farY;  // Y increment between the object's point and the opposite corner of the window border
};
   
Margins GetMargins(const ENUM_BASE_CORNER cornerconst ENUM_ANCHOR_POINT anchor,
   int dxint dy)
{
   Margins margins = {}; // zero corrections by default
   ...
   return margins;
}

为统一算法实现,引入以下方向(侧边)的宏定义:

   #define LEFT 0x1
   #define LOWER 0x2
   #define RIGHT 0x4
   #define UPPER 0x8
   #define CENTER 0x16

通过这些宏,可定义位掩码(组合),用于描述 ENUM_BASE_CORNER 和 ENUM_ANCHOR_POINT 枚举中的元素。

   const int corner_flags[] = // flags for ENUM_BASE_CORNER elements
   {
      LEFT | UPPER,
      LEFT | LOWER,
      RIGHT | LOWER,
      RIGHT | UPPER
   };
   
   const int anchor_flags[] = // flags for ENUM_ANCHOR_POINT elements
   {
      LEFT | UPPER,
      LEFT,
      LEFT | LOWER,
      LOWER,
      RIGHT | LOWER,
      RIGHT,
      RIGHT | UPPER,
      UPPER,
      CENTER
   };

数组 corner_flagsanchor_flags 各自的元素数量与对应枚举中的元素数量完全一致。

接下来是主函数代码。首先,处理最简单的情况:中心锚点。

   if(anchor == ANCHOR_CENTER)
   {
      margins.nearX = margins.farX = dx / 2;
      margins.nearY = margins.farY = dy / 2;
   }
   else
   {
      ...
   }

对于其他情况,我们将通过接收到的 corneranchor 值直接访问上述数组中的位掩码进行分析。

      const int mask = corner_flags[corner] & anchor_flags[anchor];
      ...

如果角点与锚点位于同一水平侧边,则以下条件适用,并且将在窗口远端调整对象宽度。

      if((mask & (LEFT | RIGHT)) != 0)
      {
         margins.farX = dx;
      }
      ...

如果两者不在同侧,则可能处于相反侧,或锚点位于水平边(上/下)的中间位置。通过表达式 (anchor_flags[anchor] & (LEFT | RIGHT)) == 0可检查锚点是否位于中点,如果是,则调整量为对象宽度的一半。

      else
      {
         if((anchor_flags[anchor] & (LEFT | RIGHT)) == 0)
         {
            margins.nearX = dx / 2;
            margins.farX = dx / 2;
         }
         else
         {
            margins.nearX = dx;
         }
      }
      ...

否则,如果角点与锚点方向相反,则需在窗口近端按对象宽度进行调整。

对 Y 轴执行类似的检查。

      if((mask & (UPPER | LOWER)) != 0)
      {
         margins.farY = dy;
      }
      else
      {
         if((anchor_flags[anchor] & (UPPER | LOWER)) == 0)
         {
            margins.farY = dy / 2;
            margins.nearY = dy / 2;
         }
         else
         {
            margins.nearY = dy;
         }
      }

现在,GetMargins函数已准备就绪,我们可以在OnStart 函数中继续编写脚本主代码。与之前一样,我们先确定窗口尺寸、计算中心初始坐标、创建 OBJ_LABEL 对象并选中它。

void OnStart()
{
   const int t = ChartWindowOnDropped();
   Comment(EnumToString(Corner));
   
   const string name = "ObjSizeLabel";
   int h = (int)ChartGetInteger(0CHART_HEIGHT_IN_PIXELSt) - 1;
   int w = (int)ChartGetInteger(0CHART_WIDTH_IN_PIXELS) - 1;
   int x = w / 2;
   int y = h / 2;
      
   ObjectCreate(0nameOBJ_LABELt00);
   ObjectSetInteger(0nameOBJPROP_SELECTABLEtrue);
   ObjectSetInteger(0nameOBJPROP_SELECTEDtrue);
   ObjectSetInteger(0nameOBJPROP_CORNERCorner);
   ...

为实现动画效果,通过无限循环提供 pass(迭代计数器)和 anchor(锚点周期性随机选择)变量。

   int pass = 0;
   ENUM_ANCHOR_POINT anchor = 0;
   ...

但与 ObjectAnchorLabel.mq5相比存在一些变化。

我们将不再生成对象的随机移动。而是设置 5 像素的恒定对角线移动速度。

   int px = 5py = 5;

为存储文本标签的尺寸,我们将预留两个新变量。

   int dx = 0dy = 0;

额外缩进的统计结果将存储在 Margins类型的变量 m 中。

   Margins m = {};

接下来直接进入移动和修改对象的循环。在循环中,每 75 次迭代(每次迭代 100 毫秒,详见后文)会随机选择一个新锚点,根据该锚点生成新文本(对象内容),然后等待将更改应用于对象(调用ChartRedraw)。此步骤必不可少,因为标签尺寸大小会随内容自动调整,而新尺寸对于 GetMargins调用中正确计算缩进至关重要。

我们通过调用 ObjectGetInteger并使用性 OBJPROP_XSIZE 和 OBJPROP_YSIZE 来获取尺寸。

   for( ;!IsStopped(); ++pass)
   {
      if(pass % 75 == 0)
      {
         // ENUM_ANCHOR_POINT consists of 9 elements: randomly choose one
         const int r = rand() * 8 / 32768 + 1;
         anchor = (ENUM_ANCHOR_POINT)((anchor + r) % 9);
         ObjectSetInteger(0nameOBJPROP_ANCHORanchor);
         ObjectSetString(0nameOBJPROP_TEXT" " + EnumToString(anchor)
            + StringFormat("[%3d,%3d] "xy));
         ChartRedraw();
         Sleep(1);
   
         dx = (int)ObjectGetInteger(0nameOBJPROP_XSIZE);
         dy = (int)ObjectGetInteger(0nameOBJPROP_YSIZE);
         
         m = GetMargins(Corneranchordxdy);
      }
      ...

确定锚点位置及所有距离后,即可移动对象。如果对象“碰撞”到边界,移动方向改为相反方向(根据碰撞侧边将 px转为 -px 或将 py 转为 -py)。

      // bounce off window borders, object fully visible
      if(x + px >= w - m.farX)
      {
         x = w - m.farX + px - 1;
         px = -px;
      }
      else if(x + px < m.nearX)
      {
         x = m.nearX + px;
         px = -px;
      }
      
      if(y + py >= h - m.farY)
      {
         y = h - m.farY + py - 1;
         py = -py;
      }
      else if(y + py < m.nearY)
      {
         y = m.nearY + py;
         py = -py;
      }
      
      // calculate the new label position
      x += px;
      y += py;
      ...

最后需要更新对象状态:在文本标签中显示当前坐标,并将其赋值给 OBJPROP_XDISTANCE 和 OBJPROP_YDISTANCE 特性。

      ObjectSetString(0nameOBJPROP_TEXT" " + EnumToString(anchor)
         + StringFormat("[%3d,%3d] "xy));
      ObjectSetInteger(0nameOBJPROP_XDISTANCEx);
      ObjectSetInteger(0nameOBJPROP_YDISTANCEy);
      ...

修改对象后,我们将调用 ChartRedraw并等待 100 毫秒,以确保动画流畅运行。

      ChartRedraw();
      Sleep(100);
      ...

在循环结束时,我们将再次检查窗口尺寸(因为脚本运行时用户可能改变窗口大小),并重新获取对象尺寸。

      h = (int)ChartGetInteger(0CHART_HEIGHT_IN_PIXELSt) - 1;
      w = (int)ChartGetInteger(0CHART_WIDTH_IN_PIXELS) - 1;
      
      dx = (int)ObjectGetInteger(0nameOBJPROP_XSIZE);
      dy = (int)ObjectGetInteger(0nameOBJPROP_YSIZE);
      m = GetMargins(Corneranchordxdy);
   }

为保持说明简洁,我们省略了ObjectSizeLabel.mq5脚本的其他一些改进功能。感兴趣的读者可以参考代码。具体来说,标注使用了差异化颜色:每个锚点都对应特定的颜色,从而使锚点切换更加直观可见。此外,可在脚本运行过程中点击 Delete,以将所选对象从图表中移除,且脚本将自动终止。