English Русский Deutsch
preview
MQL5取引ツール(第18回):向き制御対応の角丸吹き出し

MQL5取引ツール(第18回):向き制御対応の角丸吹き出し

MetaTrader 5 |
23 0
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

第17回では、Canvasを使用してMetaQuotes Language 5 (MQL5)上で角丸長方形と三角形をベクター方式で描画する方法について解説し、スーパーサンプリングによるアンチエイリアス処理も紹介しました。第18回となる本記事では、これら2つの図形を組み合わせ、ポインタの向きを上、下、左、右に切り替えられる角丸の吹き出しを作成します。この実装では、ポインタの位置、枠線、および不透明度を調整できるため、トレードインターフェース向けの柔軟なUI部品として活用できます。本記事では以下のトピックを扱います。

  1. 向きを制御できる角丸吹き出しの仕組み
  2. MQL5での実装
  3. バックテスト
  4. 結論

本記事を読み終える頃には、高度なアプリケーション向けに自由にカスタマイズできる、実用的な吹き出しシステムを構築できるようになります。それでは始めましょう。


向きを制御できる角丸吹き出しの仕組み

向きを制御できる角丸吹き出しは、角丸長方形の本体と三角形のポインタを組み合わせた図形です。列挙型によってポインタの向きを上、下、左、右へ動的に切り替えることができるため、トレードインターフェースにおけるアラートやツールチップなど、多用途なUI部品として利用できます。ポインタの向きによってレイアウトが決まり、本体はポインタに対して適切な位置へ移動します。また、位置合わせのためのベースオフセットが適用され、両方の図形が滑らかに結合されることで、接続部分に視覚的な不連続が生じないようになっています。このベクター方式では、スーパーサンプリングによるアンチエイリアス処理を利用することで、拡大・縮小しても高品質な描画を維持でき、MQL5アプリケーションにおける視認性とデザイン性を向上させることができます。本記事では、まずポインタの向きに応じて吹き出し本体とポインタのジオメトリを事前計算し、その後、水平または垂直方向に対応したスキャンラインアルゴリズムを用いて図形全体を塗りつぶします。さらに、枠線は分割して描画し、各辺を適切に延長することで、接続部分を継ぎ目なく滑らかに仕上げます。以下は、本記事で目指す吹き出しの完成イメージです。

さまざまな吹き出しの設定目標


MQL5での実装

MQL5で本プログラムを拡張するために、まず新しい吹き出し機能とポインタの向きを制御できるよう、新たな#define、グローバル変数、および入力パラメータを追加します。

//+------------------------------------------------------------------+
//|                           Rounded Rectangle & Triangle PART2.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property strict

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum BUBBLE_ORIENTATION {
   ORIENT_UP    = 0, // Pointer faces up
   ORIENT_DOWN  = 1, // Pointer faces down
   ORIENT_LEFT  = 2, // Pointer faces left
   ORIENT_RIGHT = 3  // Pointer faces right
};

input group "Bubble Shape"
input int              bubbleBodyWidthPixels            = 250;          // Bubble body width
input int              bubbleBodyHeightPixels           = 100;          // Bubble body height
input int              bubbleBodyCornerRadiusPixels     = 5;            // Bubble body corner radius
input int              bubblePointerBaseWidthPixels     = 60;           // Bubble pointer base width
input int              bubblePointerHeightPixels        = 40;           // Bubble pointer height
input int              bubblePointerApexRadiusPixels    = 12;           // Bubble pointer apex radius
input int              bubblePointerBaseOffsetPixels    = 0;            // Bubble pointer offset from center (0=centered, +/-=shift)
input BUBBLE_ORIENTATION bubblePointerOrientation       = ORIENT_UP;    // Bubble pointer orientation
input bool             bubbleShowBorder                 = true;         // Show bubble border
input int              bubbleBorderThicknessPixels      = 2;            // Bubble border thickness
input color            bubbleBorderColor                = clrGreen;     // Bubble border color
input int              bubbleBorderOpacityPercent       = 80;           // Bubble border opacity (0-100%)
input color            bubbleBackgroundColor            = clrGreen;     // Bubble background color
input int              bubbleBackgroundOpacityPercent   = 30;           // Bubble background opacity (0-100%)

input group "General"
input double borderExtensionMultiplier        = 0.23;    // Border extension multiplier (fraction of thickness)

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CCanvas bubbleCanvas,    bubbleHighResCanvas;                   //--- Declare bubble canvas objects

string  bubbleCanvasName    = "BubbleCanvas";                   //--- Set bubble canvas name

double bubbleBodyLeft, bubbleBodyTop, bubbleBodyRight, bubbleBodyBottom; //--- Store bubble body coordinates
double bubblePointerVerticesX[3], bubblePointerVerticesY[3];    //--- Store bubble pointer vertices X and Y
double bubblePointerArcCentersX[3], bubblePointerArcCentersY[3]; //--- Store bubble pointer arc centers X and Y
double bubblePointerTangentPointsX[3][2], bubblePointerTangentPointsY[3][2]; //--- Store bubble pointer tangent points X and Y
double bubblePointerArcStartAngles[3], bubblePointerArcEndAngles[3]; //--- Store bubble pointer arc start and end angles
int    bubblePointerApexIndex;                                  //--- Store bubble pointer apex index
double bubblePointerBaseStart, bubblePointerBaseEnd;            //--- Store bubble pointer base start and end
bool   bubbleIsHorizontalOrientation;                           //--- Store if bubble orientation is horizontal

続いて、ポインタの向きを表すBUBBLE_ORIENTATION列挙型を定義します。この列挙型には、ORIENT_UP(0)、ORIENT_DOWN(1)、ORIENT_LEFT(2)、ORIENT_RIGHT(3)の各オプションを用意し、吹き出しのレイアウトを柔軟に制御できるようにします。次に、「Bubble Shape」入力グループでは、bubbleBodyWidthPixelsやbubbleBodyHeightPixelsといった吹き出し本体の寸法を指定するパラメータを追加します。そのほか、角丸半径を指定するbubbleBodyCornerRadiusPixels、ポインタの仕様を設定するbubblePointerBaseWidthPixels、bubblePointerHeightPixels、bubblePointerApexRadiusPixels、さらにポインタの中央位置を調整するオフセットbubblePointerBaseOffsetPixelsも追加します。また、列挙型で指定するポインタの向きに加え、枠線の表示/非表示、枠線の太さ、色、および不透明度など、これまでの図形と同様の設定項目も用意します。

枠線をより細かく調整するため、borderExtensionMultiplierという共通の入力パラメータも追加します。この値は枠線の太さに対する比率として使用され、辺を適度に延長することで、接合部を継ぎ目なく滑らかに描画できるようにします。グローバル変数にも、吹き出し専用のCanvasオブジェクトであるbubbleCanvasとbubbleHighResCanvas(「BubbleCanvas」)を追加します。また、吹き出し本体の境界はbubbleBodyLeftやbubbleBodyTopなどのdouble型変数に保持します。さらに、ポインタの頂点座標、円弧の中心座標、接線(3×2配列)、角度、ポインタ先端のインデックスbubblePointerApexIndex、ポインタ基部の開始点・終了点、そしてスキャンラインの方向を動的に切り替えるための真偽値bubbleIsHorizontalOrientationも保持します。次に、前回までの図形と同様の考え方に基づき、吹き出しを生成するための関数を定義していきます。まずは、吹き出しのジオメトリを事前計算する処理から実装します。

//+------------------------------------------------------------------+
//| Bubble Shape                                                     |
//+------------------------------------------------------------------+
void PrecomputeBubbleGeometry() {
   int scalingFactor = supersamplingFactor;                     //--- Set scaling factor
   double baseOffset = 10.0 * scalingFactor;                    //--- Set base offset scaled
   
   double centeringAdjustment;                                  //--- Declare centering adjustment

   if(bubblePointerOrientation == ORIENT_UP) {                  //--- Check for up orientation
      bubbleBodyLeft = baseOffset;                              //--- Set body left
      bubbleBodyTop = baseOffset + bubblePointerHeightPixels * scalingFactor; //--- Set body top
      bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right
      bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom

      centeringAdjustment = (bubbleBodyWidthPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering
      double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset
      
      double pointerCenterX = bubbleBodyLeft + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center X
      
      bubblePointerVerticesX[0] = pointerCenterX;               //--- Set apex X
      bubblePointerVerticesY[0] = baseOffset;                   //--- Set apex Y
      
      bubblePointerVerticesX[1] = bubbleBodyLeft + actualOffset; //--- Set left X
      bubblePointerVerticesY[1] = bubbleBodyTop;                //--- Set left Y
      
      bubblePointerVerticesX[2] = bubblePointerVerticesX[1] + bubblePointerBaseWidthPixels * scalingFactor; //--- Set right X
      bubblePointerVerticesY[2] = bubbleBodyTop;                //--- Set right Y
      
      bubblePointerApexIndex = 0;                               //--- Set apex index
      bubblePointerBaseStart = bubblePointerVerticesX[1];       //--- Set base start
      bubblePointerBaseEnd = bubblePointerVerticesX[2];         //--- Set base end
      
   } else if(bubblePointerOrientation == ORIENT_DOWN) {         //--- Check for down orientation
      bubbleBodyLeft = baseOffset;                              //--- Set body left
      bubbleBodyTop = baseOffset;                               //--- Set body top
      bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right
      bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom

      centeringAdjustment = (bubbleBodyWidthPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering
      double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset
      
      double pointerCenterX = bubbleBodyLeft + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center X
      
      bubblePointerVerticesX[0] = pointerCenterX;               //--- Set apex X
      bubblePointerVerticesY[0] = bubbleBodyBottom + bubblePointerHeightPixels * scalingFactor; //--- Set apex Y
      
      bubblePointerVerticesX[1] = bubbleBodyLeft + actualOffset + bubblePointerBaseWidthPixels * scalingFactor; //--- Set right X
      bubblePointerVerticesY[1] = bubbleBodyBottom;             //--- Set right Y
      
      bubblePointerVerticesX[2] = bubbleBodyLeft + actualOffset; //--- Set left X
      bubblePointerVerticesY[2] = bubbleBodyBottom;             //--- Set left Y
      
      bubblePointerApexIndex = 0;                               //--- Set apex index
      bubblePointerBaseStart = bubblePointerVerticesX[2];       //--- Set base start
      bubblePointerBaseEnd = bubblePointerVerticesX[1];         //--- Set base end
      
   } else if(bubblePointerOrientation == ORIENT_LEFT) {         //--- Check for left orientation
      bubbleBodyLeft = baseOffset + bubblePointerHeightPixels * scalingFactor; //--- Set body left
      bubbleBodyTop = baseOffset;                               //--- Set body top
      bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right
      bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom

      centeringAdjustment = (bubbleBodyHeightPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering
      double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset
      
      double pointerCenterY = bubbleBodyTop + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center Y
      
      bubblePointerVerticesX[0] = baseOffset;                   //--- Set apex X
      bubblePointerVerticesY[0] = pointerCenterY;               //--- Set apex Y
      
      bubblePointerVerticesX[1] = bubbleBodyLeft;               //--- Set bottom X
      bubblePointerVerticesY[1] = bubbleBodyTop + actualOffset + bubblePointerBaseWidthPixels * scalingFactor; //--- Set bottom Y
      
      bubblePointerVerticesX[2] = bubbleBodyLeft;               //--- Set top X
      bubblePointerVerticesY[2] = bubbleBodyTop + actualOffset; //--- Set top Y
      
      bubblePointerApexIndex = 0;                               //--- Set apex index
      bubblePointerBaseStart = bubblePointerVerticesY[2];       //--- Set base start
      bubblePointerBaseEnd = bubblePointerVerticesY[1];         //--- Set base end
      
   } else {                                                     //--- Handle right orientation
      bubbleBodyLeft = baseOffset;                              //--- Set body left
      bubbleBodyTop = baseOffset;                               //--- Set body top
      bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right
      bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom

      centeringAdjustment = (bubbleBodyHeightPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering
      double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset
      
      double pointerCenterY = bubbleBodyTop + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center Y
      
      bubblePointerVerticesX[0] = bubbleBodyRight + bubblePointerHeightPixels * scalingFactor; //--- Set apex X
      bubblePointerVerticesY[0] = pointerCenterY;               //--- Set apex Y
      
      bubblePointerVerticesX[1] = bubbleBodyRight;              //--- Set top X
      bubblePointerVerticesY[1] = bubbleBodyTop + actualOffset; //--- Set top Y
      
      bubblePointerVerticesX[2] = bubbleBodyRight;              //--- Set bottom X
      bubblePointerVerticesY[2] = bubbleBodyTop + actualOffset + bubblePointerBaseWidthPixels * scalingFactor; //--- Set bottom Y
      
      bubblePointerApexIndex = 0;                               //--- Set apex index
      bubblePointerBaseStart = bubblePointerVerticesY[1];       //--- Set base start
      bubblePointerBaseEnd = bubblePointerVerticesY[2];         //--- Set base end
   }

   ComputeBubbleTriangleRoundedCorners();                       //--- Compute rounded corners for bubble pointer
}

void ComputeBubbleTriangleRoundedCorners() {
   double scaledRadius = (double)bubblePointerApexRadiusPixels * supersamplingFactor; //--- Scale apex radius

   int cornerIndex = bubblePointerApexIndex;                    //--- Set corner index to apex
   
   int previousIndex = (cornerIndex + 2) % 3;                   //--- Get previous index
   int nextIndex = (cornerIndex + 1) % 3;                       //--- Get next index

   double edgeA_X = bubblePointerVerticesX[cornerIndex] - bubblePointerVerticesX[previousIndex]; //--- Compute edge A X
   double edgeA_Y = bubblePointerVerticesY[cornerIndex] - bubblePointerVerticesY[previousIndex]; //--- Compute edge A Y
   double edgeA_Length = MathSqrt(edgeA_X*edgeA_X + edgeA_Y*edgeA_Y); //--- Compute edge A length
   edgeA_X /= edgeA_Length;  edgeA_Y /= edgeA_Length;           //--- Normalize edge A

   double edgeB_X = bubblePointerVerticesX[nextIndex] - bubblePointerVerticesX[cornerIndex]; //--- Compute edge B X
   double edgeB_Y = bubblePointerVerticesY[nextIndex] - bubblePointerVerticesY[cornerIndex]; //--- Compute edge B Y
   double edgeB_Length = MathSqrt(edgeB_X*edgeB_X + edgeB_Y*edgeB_Y); //--- Compute edge B length
   edgeB_X /= edgeB_Length;  edgeB_Y /= edgeB_Length;           //--- Normalize edge B

   double normalA_X =  edgeA_Y,  normalA_Y = -edgeA_X;          //--- Compute normal A
   double normalB_X =  edgeB_Y,  normalB_Y = -edgeB_X;          //--- Compute normal B

   double bisectorX = normalA_X + normalB_X,  bisectorY = normalA_Y + normalB_Y; //--- Compute bisector
   double bisectorLength = MathSqrt(bisectorX*bisectorX + bisectorY*bisectorY); //--- Compute bisector length
   if(bisectorLength < 1e-12) { bisectorX = normalA_X; bisectorY = normalA_Y; bisectorLength = MathSqrt(bisectorX*bisectorX + bisectorY*bisectorY); } //--- Handle small bisector
   bisectorX /= bisectorLength;  bisectorY /= bisectorLength;   //--- Normalize bisector

   double cosInteriorAngle = (-edgeA_X)*edgeB_X + (-edgeA_Y)*edgeB_Y; //--- Compute cosine of interior angle
   if(cosInteriorAngle >  1.0) cosInteriorAngle =  1.0;         //--- Clamp cosine upper
   if(cosInteriorAngle < -1.0) cosInteriorAngle = -1.0;         //--- Clamp cosine lower
   double halfAngle = MathArccos(cosInteriorAngle) / 2.0;       //--- Compute half angle
   double sinHalfAngle = MathSin(halfAngle);                    //--- Compute sine of half angle
   if(sinHalfAngle < 1e-12) sinHalfAngle = 1e-12;               //--- Set minimum sine value

   double distanceToCenter = scaledRadius / sinHalfAngle;       //--- Compute distance to arc center
   bubblePointerArcCentersX[cornerIndex] = bubblePointerVerticesX[cornerIndex] + bisectorX * distanceToCenter; //--- Set arc center X
   bubblePointerArcCentersY[cornerIndex] = bubblePointerVerticesY[cornerIndex] + bisectorY * distanceToCenter; //--- Set arc center Y

   double deltaX_A = bubblePointerVerticesX[cornerIndex] - bubblePointerVerticesX[previousIndex]; //--- Compute delta A X
   double deltaY_A = bubblePointerVerticesY[cornerIndex] - bubblePointerVerticesY[previousIndex]; //--- Compute delta A Y
   double lengthSquared_A = deltaX_A*deltaX_A + deltaY_A*deltaY_A; //--- Compute length squared A
   double interpolationFactor_A = ((bubblePointerArcCentersX[cornerIndex] - bubblePointerVerticesX[previousIndex])*deltaX_A + 
                                   (bubblePointerArcCentersY[cornerIndex] - bubblePointerVerticesY[previousIndex])*deltaY_A) / lengthSquared_A; //--- Compute factor A
   bubblePointerTangentPointsX[cornerIndex][1] = bubblePointerVerticesX[previousIndex] + interpolationFactor_A * deltaX_A; //--- Set tangent point X arriving
   bubblePointerTangentPointsY[cornerIndex][1] = bubblePointerVerticesY[previousIndex] + interpolationFactor_A * deltaY_A; //--- Set tangent point Y arriving

   double deltaX_B = bubblePointerVerticesX[nextIndex] - bubblePointerVerticesX[cornerIndex]; //--- Compute delta B X
   double deltaY_B = bubblePointerVerticesY[nextIndex] - bubblePointerVerticesY[cornerIndex]; //--- Compute delta B Y
   double lengthSquared_B = deltaX_B*deltaX_B + deltaY_B*deltaY_B; //--- Compute length squared B
   double interpolationFactor_B = ((bubblePointerArcCentersX[cornerIndex] - bubblePointerVerticesX[cornerIndex])*deltaX_B + 
                                   (bubblePointerArcCentersY[cornerIndex] - bubblePointerVerticesY[cornerIndex])*deltaY_B) / lengthSquared_B; //--- Compute factor B
   bubblePointerTangentPointsX[cornerIndex][0] = bubblePointerVerticesX[cornerIndex] + interpolationFactor_B * deltaX_B; //--- Set tangent point X leaving
   bubblePointerTangentPointsY[cornerIndex][0] = bubblePointerVerticesY[cornerIndex] + interpolationFactor_B * deltaY_B; //--- Set tangent point Y leaving

   bubblePointerArcStartAngles[cornerIndex] = MathArctan2(bubblePointerTangentPointsY[cornerIndex][1] - bubblePointerArcCentersY[cornerIndex], 
                                                          bubblePointerTangentPointsX[cornerIndex][1] - bubblePointerArcCentersX[cornerIndex]); //--- Set start angle
   bubblePointerArcEndAngles[cornerIndex] = MathArctan2(bubblePointerTangentPointsY[cornerIndex][0] - bubblePointerArcCentersY[cornerIndex], 
                                                        bubblePointerTangentPointsX[cornerIndex][0] - bubblePointerArcCentersX[cornerIndex]); //--- Set end angle
   
   for(int i = 0; i < 3; i++) {                                 //--- Loop over corners
      if(i == bubblePointerApexIndex) continue;                 //--- Skip apex corner
      
      bubblePointerTangentPointsX[i][0] = bubblePointerVerticesX[i]; //--- Set tangent X leaving to vertex
      bubblePointerTangentPointsY[i][0] = bubblePointerVerticesY[i]; //--- Set tangent Y leaving to vertex
      bubblePointerTangentPointsX[i][1] = bubblePointerVerticesX[i]; //--- Set tangent X arriving to vertex
      bubblePointerTangentPointsY[i][1] = bubblePointerVerticesY[i]; //--- Set tangent Y arriving to vertex
   }
}

まず、吹き出しのレイアウトを構築するために、PrecomputeBubbleGeometry関数を実装します。この関数では、最初にスーパーサンプリング係数をローカル変数へ割り当て、パディング用の基準オフセットをスケーリングします。その後、ポインタを配置するための中央位置調整値を定義します。bubblePointerOrientationの値に応じて、吹き出し本体の座標を設定します。 ORIENT_UPの場合、ポインタの下側に本体を配置します。スケーリングされた入力値を使用して、左、上、右、下の各座標を計算します。その後、適用されたオフセットを考慮して中央位置を調整し、ポインタの中心X座標を決定します。そして、ポインタ先端を上部中央に配置し、左右の頂点を本体上辺に配置する形で頂点を定義します。この場合、bubblePointerApexIndexを0に設定し、ポインタ基部の開始点と終了点を左から右方向のX座標として設定します。

ORIENT_DOWNの場合は、本体をポインタの上側に配置します。先端位置を下側へ反転させ、頂点の順序を一貫した描画方向になるよう調整します。また、ポインタ基部の開始点と終了点も適切に更新します。同様に、ORIENT_LEFTの場合は、本体をポインタの右側へ移動させます。ポインタ先端を左側に設定し、本体左辺に下側および上側の頂点を配置します。また、基部はY座標によって定義します。ORIENT_RIGHTの場合は、これを左右反転した形で処理し、必要な調整を行います。この条件分岐による設定により、方向に応じて柔軟にジオメトリを変更できるようになります。その結果、ポインタと本体を重なりなく滑らかに統合できる構造になります。

次に、ポインタ先端を丸めるため、ComputeBubbleTriangleRoundedCorners関数を呼び出します。この処理では、先端の角丸半径をスケーリングし、bubblePointerApexIndexによって指定されたポインタ先端の角に重点を置いて計算を行います。三角形の事前計算と同様に、辺のベクトルと法線を計算し、二等分線を形成します。その後、中心位置までの距離を求めるために半角の正弦値を計算し、投影処理によって正接を求めます。そして、MathArctan2関数を使用して開始角度と終了角度を設定します。最後に、先端以外の角(ポインタ基部)については、接線を直接頂点に設定します。これらの部分では角丸処理を無効化し、吹き出し本体と平滑に接続されるようにします。これは、接続部分に不要な曲線を発生させず、自然で美しい吹き出し形状を実現するために重要な処理です。円弧ベースのピクセル描画をおこなうためには、ヘルパー関数を用意する必要があります。

bool BubbleAngleInArcSweep(int cornerIndex, double angle) {
   double twoPi = 2.0 * M_PI;                                   //--- Define two pi constant
   double startAngleMod = MathMod(bubblePointerArcStartAngles[cornerIndex] + twoPi, twoPi); //--- Modulo start angle
   double endAngleMod = MathMod(bubblePointerArcEndAngles[cornerIndex] + twoPi, twoPi); //--- Modulo end angle
   angle = MathMod(angle + twoPi, twoPi);                       //--- Modulo angle

   double ccwSpan = MathMod(endAngleMod - startAngleMod + twoPi, twoPi); //--- Compute CCW span

   if(ccwSpan <= M_PI) {                                        //--- Check if short way is CCW
      double relativeAngle = MathMod(angle - startAngleMod + twoPi, twoPi); //--- Compute relative angle
      return(relativeAngle <= ccwSpan + 1e-6);                  //--- Return if within CCW span
   } else {                                                     //--- Else short way is CW
      double cwSpan = twoPi - ccwSpan;                          //--- Compute CW span
      double relativeAngle = MathMod(angle - endAngleMod + twoPi, twoPi); //--- Compute relative angle
      return(relativeAngle <= cwSpan + 1e-6);                   //--- Return if within CW span
   }
}

BubbleAngleInArcSweep関数を実装し、吹き出しポインタの特定の角において、指定された角度が円弧の範囲内に含まれるかどうかを判定します。まず、2πを表す定数を定義し、MathModを使用して開始角度、終了角度、および入力された角度を正規化します。これにより、すべての角度が0から2πの範囲内になるよう調整され、角度のラップ処理や負の値にも対応できます。次に、正規化された開始角度と終了角度の間の反時計回り方向の範囲(CCWスパン)を計算します。その値がπ以下の場合(つまり短い方の円弧を示す場合)は、開始角度からの相対角度がこの範囲内にあるかを、小さなイプシロン値を用いて精度を考慮しながら確認します。一方、範囲がπより大きい場合は、時計回り方向の範囲を2π - CCWスパンとして計算し、終了角度からの相対角度を確認します。このロジックにより、時計回り・反時計回りの両方の円弧方向に対応でき、描画時に正確なピクセル判定を行うことが可能になります。これは、吹き出しポインタの曲線的な枠線を、向きに関係なく滑らかでアーティファクト(描画欠陥)のない状態で描画するために重要な処理です。次に、角丸長方形の吹き出しを描画するために、以下の方法を使用します。

void FillBubbleRoundedRectangle(double left, double top, double width, double height, int radius, uint fillColor) {
   bubbleHighResCanvas.FillRectangle((int)(left + radius), (int)top, (int)(left + width - radius), (int)(top + height), fillColor); //--- Fill central rectangle
   bubbleHighResCanvas.FillRectangle((int)left, (int)(top + radius), (int)(left + radius), (int)(top + height - radius), fillColor); //--- Fill left strip
   bubbleHighResCanvas.FillRectangle((int)(left + width - radius), (int)(top + radius), (int)(left + width), (int)(top + height - radius), fillColor); //--- Fill right strip

   FillBubbleCircleQuadrant((int)(left + radius), (int)(top + radius), radius, fillColor, 2); //--- Fill top-left quadrant
   FillBubbleCircleQuadrant((int)(left + width - radius), (int)(top + radius), radius, fillColor, 1); //--- Fill top-right quadrant
   FillBubbleCircleQuadrant((int)(left + radius), (int)(top + height - radius), radius, fillColor, 3); //--- Fill bottom-left quadrant
   FillBubbleCircleQuadrant((int)(left + width - radius), (int)(top + height - radius), radius, fillColor, 4); //--- Fill bottom-right quadrant
}

void FillBubbleCircleQuadrant(int centerX, int centerY, int radius, uint fillColor, int quadrant) {
   double radiusDouble = (double)radius;                        //--- Convert radius to double

   for(int deltaY = -radius - 1; deltaY <= radius + 1; deltaY++) { //--- Loop over delta Y
      for(int deltaX = -radius - 1; deltaX <= radius + 1; deltaX++) { //--- Loop over delta X
         bool inQuadrant = false;                               //--- Initialize quadrant flag
         if(quadrant == 1 && deltaX >= 0 && deltaY <= 0) inQuadrant = true; //--- Check top-right
         else if(quadrant == 2 && deltaX <= 0 && deltaY <= 0) inQuadrant = true; //--- Check top-left
         else if(quadrant == 3 && deltaX <= 0 && deltaY >= 0) inQuadrant = true; //--- Check bottom-left
         else if(quadrant == 4 && deltaX >= 0 && deltaY >= 0) inQuadrant = true; //--- Check bottom-right

         if(!inQuadrant) continue;                              //--- Skip if not in quadrant

         double distance = MathSqrt(deltaX * deltaX + deltaY * deltaY); //--- Compute distance
         if(distance <= radiusDouble)                           //--- Check if within radius
            bubbleHighResCanvas.PixelSet(centerX + deltaX, centerY + deltaY, fillColor); //--- Set pixel
      }
   }
}

ここでは、FillBubbleRoundedRectangle関数を実装し、高解像度Canvas上に吹き出し本体となる角丸長方形を塗りつぶして描画します。まず、ピクセル単位で正確に描画できるよう座標を整数値へ変換します。その後、FillRectangleを使用して角部分を除いた中央領域を塗りつぶし、さらに左右の縦方向の帯状領域を追加で描画することで、側面を隙間なく滑らかにつなげます。角丸処理を完成させるために、各コーナーに対してFillBubbleCircleQuadrant関数を呼び出します。この関数では、各象限に応じた処理を適用し、必要な4分の1円のピクセルのみを塗りつぶします。

FillBubbleCircleQuadrant関数では、まず半径を精度向上のためdouble型へ変換します。その後、アンチエイリアス処理に対応するため、拡張されたデルタ範囲をループ処理します。デルタ値の符号に基づいて対象となる象限を判定します(たとえば、Xが正でYが負の場合は第1象限)。次に、MathSqrtを使用して距離を計算し、その距離が半径以内にあるピクセルに対してPixelSetを用いて描画します。これにより、長方形部分の帯状領域と自然につながる滑らかな曲線を実現できます。 このモジュール化された塗りつぶし処理は、異なる向きの吹き出しを一貫して処理するうえで重要です。なぜなら、角丸長方形の描画ロジックを再利用しながら、ポインタとの接続部分に塗り残しが発生しないよう柔軟に対応できるためです。長方形部分の枠線を描画する処理は、比較的簡単で直感的に実装できます。次に、ポインタ部分となる角丸三角形を塗りつぶすためのロジックを定義していきます。

void FillBubbleRoundedTriangle(uint fillColor) {
   if(bubbleIsHorizontalOrientation) {                          //--- Check for horizontal orientation
      double minX = bubblePointerVerticesX[0], maxX = bubblePointerVerticesX[0]; //--- Initialize min and max X
      for(int i = 1; i < 3; i++) {                              //--- Loop over vertices
         if(bubblePointerVerticesX[i] < minX) minX = bubblePointerVerticesX[i]; //--- Update min X
         if(bubblePointerVerticesX[i] > maxX) maxX = bubblePointerVerticesX[i]; //--- Update max X
      }

      int xStart = (int)MathCeil(minX);                         //--- Compute start X
      int xEnd   = (int)MathFloor(maxX);                        //--- Compute end X

      for(int x = xStart; x <= xEnd; x++) {                     //--- Loop over scanlines
         double scanlineX = (double)x + 0.5;                    //--- Set scanline X position
         double yIntersections[12];                             //--- Declare intersections array
         int intersectionCount = 0;                             //--- Initialize intersection count

         for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) {   //--- Loop over edges
            int nextIndex = (edgeIndex + 1) % 3;                //--- Get next index

            if(edgeIndex != bubblePointerApexIndex && nextIndex != bubblePointerApexIndex) continue; //--- Skip non-apex edges

            double startX, startY, endX, endY;                  //--- Declare edge coordinates

            if(edgeIndex == bubblePointerApexIndex) {           //--- Check if from apex
               startX = bubblePointerTangentPointsX[bubblePointerApexIndex][0]; //--- Set start X from tangent
               startY = bubblePointerTangentPointsY[bubblePointerApexIndex][0]; //--- Set start Y from tangent
               endX   = bubblePointerVerticesX[nextIndex];     //--- Set end X to next vertex
               endY   = bubblePointerVerticesY[nextIndex];     //--- Set end Y to next vertex
            } else {                                            //--- Handle to apex
               startX = bubblePointerVerticesX[edgeIndex];     //--- Set start X from vertex
               startY = bubblePointerVerticesY[edgeIndex];     //--- Set start Y from vertex
               endX   = bubblePointerTangentPointsX[bubblePointerApexIndex][1]; //--- Set end X to tangent
               endY   = bubblePointerTangentPointsY[bubblePointerApexIndex][1]; //--- Set end Y to tangent
            }

            double edgeMinX = (startX < endX) ? startX : endX;  //--- Compute edge min X
            double edgeMaxX = (startX > endX) ? startX : endX;  //--- Compute edge max X

            if(scanlineX < edgeMinX || scanlineX > edgeMaxX) continue; //--- Skip if outside edge X
            if(MathAbs(endX - startX) < 1e-12) continue;        //--- Skip if vertical edge

            double interpolationFactor = (scanlineX - startX) / (endX - startX); //--- Compute factor
            if(interpolationFactor < 0.0 || interpolationFactor > 1.0) continue; //--- Skip if outside segment

            yIntersections[intersectionCount++] = startY + interpolationFactor * (endY - startY); //--- Add intersection Y
         }

         {                                                      //--- Intersect apex arc block
            int cornerIndex = bubblePointerApexIndex;           //--- Set corner index
            double centerX  = bubblePointerArcCentersX[cornerIndex]; //--- Get center X
            double centerY  = bubblePointerArcCentersY[cornerIndex]; //--- Get center Y
            double radius   = (double)bubblePointerApexRadiusPixels * supersamplingFactor; //--- Get scaled radius
            double deltaX   = scanlineX - centerX;              //--- Compute delta X

            if(MathAbs(deltaX) <= radius) {                     //--- Check if within radius
               double deltaY = MathSqrt(radius * radius - deltaX * deltaX); //--- Compute delta Y

               double candidates[2];                            //--- Declare candidates array
               candidates[0] = centerY - deltaY;                //--- Set top candidate
               candidates[1] = centerY + deltaY;                //--- Set bottom candidate

               for(int candidateIndex = 0; candidateIndex < 2; candidateIndex++) { //--- Loop over candidates
                  double angle = MathArctan2(candidates[candidateIndex] - centerY, scanlineX - centerX); //--- Compute angle
                  if(BubbleAngleInArcSweep(cornerIndex, angle)) //--- Check if in arc sweep
                     yIntersections[intersectionCount++] = candidates[candidateIndex]; //--- Add intersection
               }
            }
         }

         for(int a = 0; a < intersectionCount - 1; a++)         //--- Sort intersections (bubble sort)
            for(int b = a + 1; b < intersectionCount; b++)       //--- Inner loop for sorting
               if(yIntersections[a] > yIntersections[b]) {       //--- Check if swap needed
                  double temp = yIntersections[a];               //--- Temporary store
                  yIntersections[a] = yIntersections[b];         //--- Swap values
                  yIntersections[b] = temp;                      //--- Complete swap
               }

         for(int pairIndex = 0; pairIndex + 1 < intersectionCount; pairIndex += 2) { //--- Loop over pairs
            int yTop    = (int)MathCeil(yIntersections[pairIndex]); //--- Compute top Y
            int yBottom = (int)MathFloor(yIntersections[pairIndex + 1]); //--- Compute bottom Y
            for(int y = yTop; y <= yBottom; y++)                //--- Loop over vertical span
               bubbleHighResCanvas.PixelSet(x, y, fillColor);   //--- Set pixel with fill color
         }
      }

   } else {                                                     //--- Handle vertical orientation
      double minY = bubblePointerVerticesY[0], maxY = bubblePointerVerticesY[0]; //--- Initialize min and max Y
      for(int i = 1; i < 3; i++) {                              //--- Loop over vertices
         if(bubblePointerVerticesY[i] < minY) minY = bubblePointerVerticesY[i]; //--- Update min Y
         if(bubblePointerVerticesY[i] > maxY) maxY = bubblePointerVerticesY[i]; //--- Update max Y
      }

      int yStart = (int)MathCeil(minY);                         //--- Compute start Y
      int yEnd = (int)MathFloor(maxY);                          //--- Compute end Y

      for(int y = yStart; y <= yEnd; y++) {                     //--- Loop over scanlines
         double scanlineY = (double)y + 0.5;                    //--- Set scanline Y position
         double xIntersections[12];                             //--- Declare intersections array
         int intersectionCount = 0;                             //--- Initialize intersection count

         for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) {   //--- Loop over edges
            int nextIndex = (edgeIndex + 1) % 3;                //--- Get next index
            
            if(edgeIndex != bubblePointerApexIndex && nextIndex != bubblePointerApexIndex) continue; //--- Skip non-apex edges
            
            double startX, startY, endX, endY;                  //--- Declare edge coordinates
            
            if(edgeIndex == bubblePointerApexIndex) {           //--- Check if from apex
               startX = bubblePointerTangentPointsX[bubblePointerApexIndex][0]; //--- Set start X from tangent
               startY = bubblePointerTangentPointsY[bubblePointerApexIndex][0]; //--- Set start Y from tangent
               endX = bubblePointerVerticesX[nextIndex];       //--- Set end X to next vertex
               endY = bubblePointerVerticesY[nextIndex];       //--- Set end Y to next vertex
            } else {                                            //--- Handle to apex
               startX = bubblePointerVerticesX[edgeIndex];     //--- Set start X from vertex
               startY = bubblePointerVerticesY[edgeIndex];     //--- Set start Y from vertex
               endX = bubblePointerTangentPointsX[bubblePointerApexIndex][1]; //--- Set end X to tangent
               endY = bubblePointerTangentPointsY[bubblePointerApexIndex][1]; //--- Set end Y to tangent
            }

            double edgeMinY = (startY < endY) ? startY : endY;  //--- Compute edge min Y
            double edgeMaxY = (startY > endY) ? startY : endY;  //--- Compute edge max Y

            if(scanlineY < edgeMinY || scanlineY > edgeMaxY) continue; //--- Skip if outside edge Y
            if(MathAbs(endY - startY) < 1e-12) continue;        //--- Skip if horizontal

            double interpolationFactor = (scanlineY - startY) / (endY - startY); //--- Compute factor
            if(interpolationFactor < 0.0 || interpolationFactor > 1.0) continue; //--- Skip if outside segment

            xIntersections[intersectionCount++] = startX + interpolationFactor * (endX - startX); //--- Add intersection X
         }

         int cornerIndex = bubblePointerApexIndex;              //--- Set corner index
         double centerX = bubblePointerArcCentersX[cornerIndex]; //--- Get center X
         double centerY = bubblePointerArcCentersY[cornerIndex]; //--- Get center Y
         double radius = (double)bubblePointerApexRadiusPixels * supersamplingFactor; //--- Get scaled radius
         double deltaY = scanlineY - centerY;                   //--- Compute delta Y

         if(MathAbs(deltaY) <= radius) {                        //--- Check if within radius
            double deltaX = MathSqrt(radius*radius - deltaY*deltaY); //--- Compute delta X

            double candidates[2];                               //--- Declare candidates array
            candidates[0] = centerX - deltaX;                   //--- Set left candidate
            candidates[1] = centerX + deltaX;                   //--- Set right candidate

            for(int candidateIndex = 0; candidateIndex < 2; candidateIndex++) { //--- Loop over candidates
               double angle = MathArctan2(scanlineY - centerY, candidates[candidateIndex] - centerX); //--- Compute angle
               if(BubbleAngleInArcSweep(cornerIndex, angle))    //--- Check if in arc sweep
                  xIntersections[intersectionCount++] = candidates[candidateIndex]; //--- Add intersection
            }
         }

         for(int a = 0; a < intersectionCount - 1; a++)         //--- Sort intersections (bubble sort)
            for(int b = a + 1; b < intersectionCount; b++)       //--- Inner loop for sorting
               if(xIntersections[a] > xIntersections[b]) {       //--- Check if swap needed
                  double temp = xIntersections[a];               //--- Temporary store
                  xIntersections[a] = xIntersections[b];         //--- Swap values
                  xIntersections[b] = temp;                      //--- Complete swap
               }

         for(int pairIndex = 0; pairIndex + 1 < intersectionCount; pairIndex += 2) { //--- Loop over pairs
            int xLeft = (int)MathCeil(xIntersections[pairIndex]); //--- Compute left X
            int xRight = (int)MathFloor(xIntersections[pairIndex + 1]); //--- Compute right X
            for(int x = xLeft; x <= xRight; x++)                //--- Loop over horizontal span
               bubbleHighResCanvas.PixelSet(x, y, fillColor);   //--- Set pixel with fill color
         }
      }
   }
}

FillBubbleRoundedTriangle関数を実装し、高解像度Canvas上で吹き出しの三角形ポインタ部分を塗りつぶします。この処理では、効率を最適化するため、吹き出しの向きに応じてスキャンライン方向を切り替えます。bubbleIsHorizontalOrientationがtrueの場合、つまりポインタが左右方向(左向き・右向き)の場合は、水平方向のスキャン(x方向のループ)を使用します。それ以外の場合は、垂直方向のスキャン(y方向のループ)を使用します。水平方向の向きの場合、まずポインタ頂点から最小X値と最大X値を取得します。その後、半ピクセル分のオフセットを加えた整数X座標についてループ処理を行います。各X位置において、補間処理によってY方向の交点を取得します。この際、先端に隣接する辺のみを対象とし、基部側の辺は除外します。また、円弧部分については、デルタXにおける円の方程式を解くことで候補となる交点を追加します。ただし、その点が半径内に存在し、かつBubbleAngleInArcSweepによって円弧範囲内であることが確認された場合のみ有効とします。取得した交点はバブルソートによって並び替え、その後、交点のペア間に存在する垂直方向の範囲をPixelSetを使用してfillColorで塗りつぶします。

垂直方向の向きの場合は、この処理を反転して適用します。まず最小Y値と最大Y値を取得し、scanlineYを用いてY方向へループします。その各位置において、辺および円弧部分からX方向の交点を取得します。この場合、円弧候補の計算にはdeltaYを使用します。取得した交点を並び替え、水平方向の範囲を塗りつぶすことで、隙間のない完全な描画を実現します。 これにより、吹き出し本体となる角丸長方形と三角形ポインタの塗りつぶし処理が完成しました。次に、吹き出し全体を囲む枠線を描画するためのロジックを定義します。これを実現するために、以下の方法を使用します。

void DrawBubbleBorder(uint borderColorARGB) {
   int scaledThickness = bubbleBorderThicknessPixels * supersamplingFactor; //--- Scale border thickness
   int scaledBodyRadius = bubbleBodyCornerRadiusPixels * supersamplingFactor; //--- Scale body radius

   if(bubblePointerOrientation == ORIENT_UP || bubblePointerOrientation == ORIENT_DOWN) { //--- Check for up or down orientation
      if(bubblePointerOrientation == ORIENT_DOWN) {             //--- Check for down orientation
         DrawBubbleHorizontalEdge(bubbleBodyLeft + scaledBodyRadius, bubbleBodyTop, bubbleBodyRight - scaledBodyRadius, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw top edge
      } else {                                                  //--- Handle up orientation
         if(bubblePointerBaseStart > bubbleBodyLeft + scaledBodyRadius) //--- Check if base start exceeds left radius
            DrawBubbleHorizontalEdge(bubbleBodyLeft + scaledBodyRadius, bubbleBodyTop, bubblePointerBaseStart, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw left segment of top edge
         if(bubblePointerBaseEnd < bubbleBodyRight - scaledBodyRadius) //--- Check if base end below right radius
            DrawBubbleHorizontalEdge(bubblePointerBaseEnd, bubbleBodyTop, bubbleBodyRight - scaledBodyRadius, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw right segment of top edge
      }

      if(bubblePointerOrientation == ORIENT_UP) {               //--- Check for up orientation
         DrawBubbleHorizontalEdge(bubbleBodyRight - scaledBodyRadius, bubbleBodyBottom, bubbleBodyLeft + scaledBodyRadius, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw bottom edge
      } else {                                                  //--- Handle down orientation
         if(bubblePointerBaseStart > bubbleBodyLeft + scaledBodyRadius) //--- Check if base start exceeds left radius
            DrawBubbleHorizontalEdge(bubblePointerBaseStart, bubbleBodyBottom, bubbleBodyLeft + scaledBodyRadius, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw left segment of bottom edge
         if(bubblePointerBaseEnd < bubbleBodyRight - scaledBodyRadius) //--- Check if base end below right radius
            DrawBubbleHorizontalEdge(bubbleBodyRight - scaledBodyRadius, bubbleBodyBottom, bubblePointerBaseEnd, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw right segment of bottom edge
      }

      DrawBubbleVerticalEdge(bubbleBodyLeft, bubbleBodyBottom - scaledBodyRadius, bubbleBodyLeft, bubbleBodyTop + scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw left edge
      DrawBubbleVerticalEdge(bubbleBodyRight, bubbleBodyTop + scaledBodyRadius, bubbleBodyRight, bubbleBodyBottom - scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw right edge

   } else {                                                     //--- Handle left or right orientation
      DrawBubbleHorizontalEdge(bubbleBodyLeft + scaledBodyRadius, bubbleBodyTop, bubbleBodyRight - scaledBodyRadius, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw top edge
      DrawBubbleHorizontalEdge(bubbleBodyRight - scaledBodyRadius, bubbleBodyBottom, bubbleBodyLeft + scaledBodyRadius, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw bottom edge

      if(bubblePointerOrientation == ORIENT_RIGHT) {            //--- Check for right orientation
         DrawBubbleVerticalEdge(bubbleBodyLeft, bubbleBodyBottom - scaledBodyRadius, bubbleBodyLeft, bubbleBodyTop + scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw left edge
      } else {                                                  //--- Handle left orientation
         if(bubblePointerBaseStart > bubbleBodyTop + scaledBodyRadius) //--- Check if base start exceeds top radius
            DrawBubbleVerticalEdge(bubbleBodyLeft, bubblePointerBaseStart, bubbleBodyLeft, bubbleBodyTop + scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw top segment of left edge
         if(bubblePointerBaseEnd < bubbleBodyBottom - scaledBodyRadius) //--- Check if base end below bottom radius
            DrawBubbleVerticalEdge(bubbleBodyLeft, bubbleBodyBottom - scaledBodyRadius, bubbleBodyLeft, bubblePointerBaseEnd, scaledThickness, borderColorARGB); //--- Draw bottom segment of left edge
      }

      if(bubblePointerOrientation == ORIENT_LEFT) {             //--- Check for left orientation
         DrawBubbleVerticalEdge(bubbleBodyRight, bubbleBodyTop + scaledBodyRadius, bubbleBodyRight, bubbleBodyBottom - scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw right edge
      } else {                                                  //--- Handle right orientation
         if(bubblePointerBaseStart > bubbleBodyTop + scaledBodyRadius) //--- Check if base start exceeds top radius
            DrawBubbleVerticalEdge(bubbleBodyRight, bubbleBodyTop + scaledBodyRadius, bubbleBodyRight, bubblePointerBaseStart, scaledThickness, borderColorARGB); //--- Draw top segment of right edge
         if(bubblePointerBaseEnd < bubbleBodyBottom - scaledBodyRadius) //--- Check if base end below bottom radius
            DrawBubbleVerticalEdge(bubbleBodyRight, bubblePointerBaseEnd, bubbleBodyRight, bubbleBodyBottom - scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw bottom segment of right edge
      }
   }

   DrawBubbleCornerArc((int)(bubbleBodyLeft + scaledBodyRadius), (int)(bubbleBodyTop + scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, M_PI, M_PI * 1.5); //--- Draw top-left arc
   DrawBubbleCornerArc((int)(bubbleBodyRight - scaledBodyRadius), (int)(bubbleBodyTop + scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, M_PI * 1.5, M_PI * 2.0); //--- Draw top-right arc
   DrawBubbleCornerArc((int)(bubbleBodyLeft + scaledBodyRadius), (int)(bubbleBodyBottom - scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, M_PI * 0.5, M_PI); //--- Draw bottom-left arc
   DrawBubbleCornerArc((int)(bubbleBodyRight - scaledBodyRadius), (int)(bubbleBodyBottom - scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, 0.0, M_PI * 0.5); //--- Draw bottom-right arc

   for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) {         //--- Loop over pointer edges
      int nextIndex = (edgeIndex + 1) % 3;                      //--- Get next index
      
      if(edgeIndex != bubblePointerApexIndex && nextIndex != bubblePointerApexIndex) continue; //--- Skip non-apex edges
      
      double startX, startY, endX, endY;                        //--- Declare edge coordinates
      
      if(edgeIndex == bubblePointerApexIndex) {                 //--- Check if from apex
         startX = bubblePointerTangentPointsX[bubblePointerApexIndex][0]; //--- Set start X from tangent
         startY = bubblePointerTangentPointsY[bubblePointerApexIndex][0]; //--- Set start Y from tangent
         endX = bubblePointerVerticesX[nextIndex];              //--- Set end X to next vertex
         endY = bubblePointerVerticesY[nextIndex];              //--- Set end Y to next vertex
      } else {                                                  //--- Handle to apex
         startX = bubblePointerVerticesX[edgeIndex];            //--- Set start X from vertex
         startY = bubblePointerVerticesY[edgeIndex];            //--- Set start Y from vertex
         endX = bubblePointerTangentPointsX[bubblePointerApexIndex][1]; //--- Set end X to tangent
         endY = bubblePointerTangentPointsY[bubblePointerApexIndex][1]; //--- Set end Y to tangent
      }

      DrawBubbleStraightEdge(startX, startY, endX, endY, scaledThickness, borderColorARGB); //--- Draw straight edge
   }

   DrawBubbleTriangleCornerArc(bubblePointerApexIndex, scaledThickness, borderColorARGB); //--- Draw apex arc
}

void DrawBubbleHorizontalEdge(double startX, double startY, double endX, double endY, int thickness, uint edgeColor) {
   DrawBubbleStraightEdge(startX, startY, endX, endY, thickness, edgeColor); //--- Draw horizontal edge using straight edge
}

void DrawBubbleVerticalEdge(double startX, double startY, double endX, double endY, int thickness, uint edgeColor) {
   DrawBubbleStraightEdge(startX, startY, endX, endY, thickness, edgeColor); //--- Draw vertical edge using straight edge
}

void DrawBubbleStraightEdge(double startX, double startY, double endX, double endY, int thickness, uint edgeColor) {
   double deltaX = endX - startX;                               //--- Compute delta X
   double deltaY = endY - startY;                               //--- Compute delta Y
   double edgeLength = MathSqrt(deltaX*deltaX + deltaY*deltaY); //--- Compute edge length
   if(edgeLength < 1e-6) return;                                //--- Return if length too small

   double perpendicularX = -deltaY / edgeLength;                //--- Compute perpendicular X
   double perpendicularY = deltaX / edgeLength;                 //--- Compute perpendicular Y

   double edgeDirectionX = deltaX / edgeLength;                 //--- Compute edge direction X
   double edgeDirectionY = deltaY / edgeLength;                 //--- Compute edge direction Y

   double halfThickness = (double)thickness / 2.0;              //--- Compute half thickness
   double extensionLength = borderExtensionMultiplier * (double)thickness; //--- Set extension length
   
   double extendedStartX = startX - edgeDirectionX * extensionLength; //--- Extend start X
   double extendedStartY = startY - edgeDirectionY * extensionLength; //--- Extend start Y
   double extendedEndX = endX + edgeDirectionX * extensionLength; //--- Extend end X
   double extendedEndY = endY + edgeDirectionY * extensionLength; //--- Extend end Y

   double verticesX[4], verticesY[4];                           //--- Declare vertices arrays
   verticesX[0] = extendedStartX - perpendicularX * halfThickness;  verticesY[0] = extendedStartY - perpendicularY * halfThickness; //--- Set vertex 0
   verticesX[1] = extendedStartX + perpendicularX * halfThickness;  verticesY[1] = extendedStartY + perpendicularY * halfThickness; //--- Set vertex 1
   verticesX[2] = extendedEndX + perpendicularX * halfThickness;  verticesY[2] = extendedEndY + perpendicularY * halfThickness; //--- Set vertex 2
   verticesX[3] = extendedEndX - perpendicularX * halfThickness;  verticesY[3] = extendedEndY - perpendicularY * halfThickness; //--- Set vertex 3

   FillQuadrilateral(bubbleHighResCanvas, verticesX, verticesY, edgeColor); //--- Fill quadrilateral for edge
}

void DrawBubbleCornerArc(int centerX, int centerY, int radius, int thickness, uint edgeColor,
                         double startAngle, double endAngle) {
   int halfThickness = thickness / 2;                           //--- Compute half thickness
   double outerRadius = (double)radius + halfThickness;         //--- Compute outer radius
   double innerRadius = (double)radius - halfThickness;         //--- Compute inner radius
   if(innerRadius < 0) innerRadius = 0;                         //--- Set inner radius to zero if negative

   int pixelRange = (int)(outerRadius + 2);                     //--- Compute pixel range

   for(int deltaY = -pixelRange; deltaY <= pixelRange; deltaY++) { //--- Loop over delta Y
      for(int deltaX = -pixelRange; deltaX <= pixelRange; deltaX++) { //--- Loop over delta X
         double distance = MathSqrt(deltaX * deltaX + deltaY * deltaY); //--- Compute distance
         if(distance < innerRadius || distance > outerRadius) continue; //--- Skip if outside radii

         double angle = MathArctan2((double)deltaY, (double)deltaX); //--- Compute angle
         
         if(IsAngleBetween(angle, startAngle, endAngle))        //--- Check if angle within range
            bubbleHighResCanvas.PixelSet(centerX + deltaX, centerY + deltaY, edgeColor); //--- Set pixel
      }
   }
}

void DrawBubbleTriangleCornerArc(int cornerIndex, int thickness, uint edgeColor) {
   double centerX = bubblePointerArcCentersX[cornerIndex];      //--- Get center X
   double centerY = bubblePointerArcCentersY[cornerIndex];      //--- Get center Y
   double radius = (double)bubblePointerApexRadiusPixels * supersamplingFactor; //--- Get scaled radius

   int halfThickness = thickness / 2;                           //--- Compute half thickness
   double outerRadius = radius + halfThickness;                 //--- Compute outer radius
   double innerRadius = radius - halfThickness;                 //--- Compute inner radius
   if(innerRadius < 0) innerRadius = 0;                         //--- Set inner radius to zero if negative

   int pixelRange = (int)(outerRadius + 2);                     //--- Compute pixel range

   for(int deltaY = -pixelRange; deltaY <= pixelRange; deltaY++) { //--- Loop over delta Y
      for(int deltaX = -pixelRange; deltaX <= pixelRange; deltaX++) { //--- Loop over delta X
         double distance = MathSqrt((double)(deltaX*deltaX + deltaY*deltaY)); //--- Compute distance
         if(distance < innerRadius || distance > outerRadius) continue; //--- Skip if outside radii

         double angle = MathArctan2((double)deltaY, (double)deltaX); //--- Compute angle
         
         if(BubbleAngleInArcSweep(cornerIndex, angle)) {        //--- Check if in arc sweep
            int pixelX = (int)MathRound(centerX + deltaX);      //--- Round to pixel X
            int pixelY = (int)MathRound(centerY + deltaY);      //--- Round to pixel Y
            if(pixelX >= 0 && pixelX < bubbleHighResCanvas.Width() && pixelY >= 0 && pixelY < bubbleHighResCanvas.Height()) //--- Check if within bounds
               bubbleHighResCanvas.PixelSet(pixelX, pixelY, edgeColor); //--- Set pixel
         }
      }
   }
}

ここでは、DrawBubbleBorder関数を実装し、高解像度Canvas上に吹き出し全体の枠線を描画します。まず、スーパーサンプリング係数を使用して線の太さと本体の角丸半径をスケーリングします。その後、吹き出しの向きに応じて分割された辺を条件付きで描画します。上向き・下向き(ORIENT_UP/ORIENT_DOWN)の場合は、上下方向の水平辺を処理します。ポインタが接続される部分では、ポインタ基部の周囲を避けるように水平辺を分割して描画し、左右の垂直辺については完全な形で描画します。左向き・右向き(ORIENT_LEFT/ORIENT_RIGHT)の場合は、水平辺を完全に描画し、垂直辺については同様にポインタ接続部分を避けるよう分割して描画します。これらの処理では、DrawBubbleHorizontalEdgeおよびDrawBubbleVerticalEdge関数を使用し、ポインタとの接続部分に枠線が重なって描画されないようにします。次に、あらかじめラジアン単位で定義した角度範囲を使用し、DrawBubbleCornerArc関数によって吹き出し本体の4つの角丸部分の円弧を描画します。続いて、3つあるポインタの辺をループ処理し、特に先端に隣接する辺を対象として、DrawBubbleStraightEdge関数を使用して接線部分から直線枠線を追加します。最後に、DrawBubbleTriangleCornerArc関数によってポインタ先端部分の円弧を描画し、枠線処理を完成させます。

辺の描画処理を統一するため、DrawBubbleHorizontalEdgeおよびDrawBubbleVerticalEdge関数では、単純にDrawBubbleStraightEdge関数を呼び出します。 DrawBubbleStraightEdgeでは、まず方向ベクトルと垂直ベクトルを計算します。その後、滑らかな接続を実現するため、端点をborderExtensionMultiplier×線の太さ分だけ延長します。次に、四角形状の帯を形成し、FillQuadrilateralを使用して辺の色で塗りつぶします。DrawBubbleCornerArcでは、内側および外側の半径を線の太さの半分から計算することで、曲線状の境界リングを作成します。その後、拡張されたピクセル領域をループ処理し、距離計算によって対象範囲内にあるピクセルを判定して描画します。また、IsAngleBetween関数によって円弧セグメント内に存在するかを確認し、正確な曲率を維持します。最後に、DrawBubbleTriangleCornerArcでは、これと同じ考え方をポインタ先端部分へ適用します。この処理では、スケーリングされた半径を使用し、ピクセルループ内でMathSqrtによって距離を計算します。さらに、MathArctan2によって角度を取得し、BubbleAngleInArcSweepによって円弧範囲内にあるかを判定します。座標にはMathRoundを使用し、境界チェックを行った後にPixelSetで描画することで、滑らかでアンチエイリアス処理された枠線を実現し、本体部分とポインタ部分が自然につながるようにします。これで、吹き出しを作成するために必要なすべてのロジックを組み合わせる準備が整いました。

void DrawBubble() {
   uint backgroundColorARGB = ColorToARGBWithOpacity(bubbleBackgroundColor, bubbleBackgroundOpacityPercent); //--- Get background ARGB
   uint borderColorARGB = ColorToARGBWithOpacity(bubbleBorderColor, bubbleBorderOpacityPercent); //--- Get border ARGB

   FillBubble(backgroundColorARGB);                             //--- Fill bubble

   if(bubbleShowBorder && bubbleBorderThicknessPixels > 0)      //--- Check if border should be shown
      DrawBubbleBorder(borderColorARGB);                        //--- Draw bubble border

   BicubicDownsample(bubbleCanvas, bubbleHighResCanvas);        //--- Downsample to display canvas

   bubbleCanvas.FontSet("Arial", 13, FW_NORMAL);                //--- Set font for text
   string displayText = "Bubble";                               //--- Set display text
   int textWidth, textHeight;                                   //--- Declare text dimensions
   bubbleCanvas.TextSize(displayText, textWidth, textHeight);   //--- Get text size
   
   int bodyDisplayLeft = (int)(bubbleBodyLeft / supersamplingFactor); //--- Compute display left
   int bodyDisplayTop = (int)(bubbleBodyTop / supersamplingFactor); //--- Compute display top
   int bodyDisplayWidth = (int)((bubbleBodyRight - bubbleBodyLeft) / supersamplingFactor); //--- Compute display width
   int bodyDisplayHeight = (int)((bubbleBodyBottom - bubbleBodyTop) / supersamplingFactor); //--- Compute display height
   
   int textPositionX = bodyDisplayLeft + (bodyDisplayWidth - textWidth) / 2; //--- Compute text X position
   int textPositionY = bodyDisplayTop + (bodyDisplayHeight - textHeight) / 2; //--- Compute text Y position
   bubbleCanvas.TextOut(textPositionX, textPositionY, displayText, (uint)0xFF000000, TA_LEFT); //--- Draw text on canvas
}

void FillBubble(uint fillColor) {
   FillBubbleRoundedRectangle(bubbleBodyLeft, bubbleBodyTop, bubbleBodyRight - bubbleBodyLeft, bubbleBodyBottom - bubbleBodyTop, 
                              bubbleBodyCornerRadiusPixels * supersamplingFactor, fillColor); //--- Fill bubble body rectangle

   FillBubbleRoundedTriangle(fillColor);                        //--- Fill bubble pointer triangle
}

描画ロジックを統合するために、Canvas上で吹き出しの描画処理全体を制御するDrawBubble関数を定義します。まず、半透明効果を実現するために、ColorToARGBWithOpacity関数を使用して背景色と枠線の色を不透明度付きのARGB形式へ変換します。図形を構築するために、背景色を使用してFillBubble関数を呼び出し、高解像度Canvas上に吹き出し全体を塗りつぶします。その後、枠線が有効になっている場合は、DrawBubbleBorder関数を呼び出して枠線を追加します。続いて、アンチエイリアス処理された出力を得るために、BicubicDownsample関数を使用して高解像度Canvasの内容を通常解像度のCanvasへ縮小変換します。最後に、通常解像度のCanvas上でArialフォントを設定し、サイズを13、太さをFW_NORMALに指定します。その後、ラベル「Bubble」のサイズを計測し、本体の境界範囲内で中央に配置できるよう、表示座標へ変換したうえで位置を計算します。そして、識別用のラベルとして、TextOutを用いて黒色で左揃えのテキストを描画します。

FillBubble関数では、まず事前計算された座標とスケーリングされた半径を使用し、FillBubbleRoundedRectangleによって吹き出し本体を塗りつぶします。その後、FillBubbleRoundedTriangleによってポインタ部分を塗りつぶし、高解像度Canvas上で両方の図形を滑らかに統合することで、一つの吹き出し形状として完成させます。これで、初期化処理内からこの関数を呼び出し、吹き出しを描画できるようになります。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   
   // added this for the bubble in initialization alongside the other prior shapes
   
   bubbleIsHorizontalOrientation = (bubblePointerOrientation == ORIENT_LEFT || bubblePointerOrientation == ORIENT_RIGHT); //--- Determine if orientation is horizontal

   int bubbleCanvasWidth, bubbleCanvasHeight;                   //--- Declare bubble canvas dimensions
   if(bubbleIsHorizontalOrientation) {                          //--- Check for horizontal orientation
      bubbleCanvasWidth = bubbleBodyWidthPixels + bubblePointerHeightPixels + 40; //--- Compute width for horizontal
      bubbleCanvasHeight = bubbleBodyHeightPixels + 40;         //--- Compute height for horizontal
   } else {                                                     //--- Handle vertical orientation
      bubbleCanvasWidth = bubbleBodyWidthPixels + 40;           //--- Compute width for vertical
      bubbleCanvasHeight = bubbleBodyHeightPixels + bubblePointerHeightPixels + 40; //--- Compute height for vertical
   }

   int bubblePositionY = trianglePositionY + triangleCanvasHeight + shapesGapPixels; //--- Set bubble Y position below triangle

   if(!bubbleCanvas.CreateBitmapLabel(0, 0, bubbleCanvasName, shapesPositionX, bubblePositionY,
                                    bubbleCanvasWidth, bubbleCanvasHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create bubble canvas bitmap label
      Print("Error creating bubble canvas: ", GetLastError());  //--- Print error message if creation fails
      return(INIT_FAILED);                                      //--- Return initialization failure
   }

   if(!bubbleHighResCanvas.Create(bubbleCanvasName + "_hires",
                        bubbleCanvasWidth * supersamplingFactor,
                        bubbleCanvasHeight * supersamplingFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create high-res bubble canvas
      Print("Error creating bubble hi-res canvas: ", GetLastError()); //--- Print error message if creation fails
      return(INIT_FAILED);                                      //--- Return initialization failure
   }

   bubbleCanvas.Erase(ColorToARGB(clrNONE, 0));                 //--- Clear bubble canvas
   bubbleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));          //--- Clear high-res bubble canvas

   PrecomputeBubbleGeometry();                                  //--- Precompute bubble geometry
   
   DrawBubble();                                                //--- Draw bubble

   bubbleCanvas.Update();                                       //--- Update bubble canvas display

   return(INIT_SUCCEEDED);                                      //--- Return initialization success
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   rectangleHighResCanvas.Destroy();                            //--- Destroy high-res rectangle canvas
   rectangleCanvas.Destroy();                                   //--- Destroy rectangle canvas
   ObjectDelete(0, rectangleCanvasName);                        //--- Delete rectangle canvas object

   triangleHighResCanvas.Destroy();                             //--- Destroy high-res triangle canvas
   triangleCanvas.Destroy();                                    //--- Destroy triangle canvas
   ObjectDelete(0, triangleCanvasName);                         //--- Delete triangle canvas object

   bubbleHighResCanvas.Destroy();                               //--- Destroy high-res bubble canvas
   bubbleCanvas.Destroy();                                      //--- Destroy bubble canvas
   ObjectDelete(0, bubbleCanvasName);                           //--- Delete bubble canvas object

   ChartRedraw();                                               //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Chart event function                                             |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam) {
   if(id == CHARTEVENT_CHART_CHANGE) {                          //--- Check for chart change event
      rectangleCanvas.Erase(ColorToARGB(clrNONE, 0));           //--- Clear rectangle canvas
      rectangleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));    //--- Clear high-res rectangle canvas
      DrawRoundedRectangle();                                   //--- Redraw rounded rectangle
      rectangleCanvas.Update();                                 //--- Update rectangle canvas display

      triangleCanvas.Erase(ColorToARGB(clrNONE, 0));            //--- Clear triangle canvas
      triangleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));     //--- Clear high-res triangle canvas
      DrawRoundedTriangle();                                    //--- Redraw rounded triangle
      triangleCanvas.Update();                                  //--- Update triangle canvas display

      bubbleCanvas.Erase(ColorToARGB(clrNONE, 0));              //--- Clear bubble canvas
      bubbleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));       //--- Clear high-res bubble canvas
      DrawBubble();                                             //--- Redraw bubble
      bubbleCanvas.Update();                                    //--- Update bubble canvas display
   }
}

OnInitイベントハンドラを拡張し、これまでの図形に加えて吹き出しの描画処理を組み込みます。まず、左向きまたは右向きのポインタであるかどうかを基準に、bubbleIsHorizontalOrientationによって水平方向の配置かどうかを判定します。その後、キャンバスサイズを適切に計算します。水平方向の場合は幅にポインタの高さを加算し、垂直方向の場合は高さにポインタの高さを加算します。また、パディングも考慮して最終的なキャンバスサイズを決定します。次に、直前のキャンバスのY座標にギャップを加えた位置を使用して、三角形の下側に吹き出しを配置します。続いて、透明度を維持するためにARGBを正規化した状態でCreateBitmapLabelを使用して通常解像度のキャンバスを作成します。また、スケーリングされたサイズを使用してCreateにより高解像度版のキャンバスを作成します。作成中にエラーが発生した場合はログへ記録し、作成に失敗した場合は初期化を失敗として終了します。描画準備として、両方のキャンバスを透明なARGBでクリアします。その後、レイアウト計算のためにPrecomputeBubbleGeometryを呼び出し、DrawBubbleによって描画を実行します。表示を更新した後、INIT_SUCCEEDEDを返します。

OnDeinitハンドラでは、終了時のクリーンアップ処理を拡張します。高解像度および通常解像度の吹き出しキャンバスを破棄し、オブジェクトを削除した後、チャートを再描画します。分かりやすくするため、今回追加した拡張部分は明示しています。

最後に、OnChartEventでは、CHARTEVENT_CHART_CHANGEイベントに対する処理へ吹き出し対応を追加します。チャート変更が発生した場合、吹き出しキャンバスをクリアし、DrawBubbleによって再描画を行います。そして、長方形および三角形の描画処理と合わせて表示を更新し、チャート変更に柔軟に対応できるようにします。コンパイルすると、次の結果が得られます。

角丸吹き出し

画像から確認できるように、吹き出しは当初の目的どおり正しく描画されています。ここで、枠線の延長長さを制御するために、もう1つ変更を加えました。固定値による延長を削除し、長さの比率を使用することで、より動的な制御ができるようにしました。この方法を採用した理由は、枠線の太さを大きくした場合、ポインタと角丸長方形の枠線との接続部分が滑らかにつながらなくなる問題が発生したためです。一貫性を保つため、DrawRectStraightEdgeおよびDrawTriStraightEdge関数内の枠線延長処理を以下のように更新しました。

double extensionLength = borderExtensionMultiplier * (double)thickness; //--- Set extension length

この更新による効果、特に吹き出しの枠線部分への影響を確認してください。

延長長さの比較

この視覚的な比較から、延長率が吹き出しの接続部分にどのような影響を与えるかが分かります。これは、特に大きな幅を持つ図形を使用する場合に非常に重要です。今回の接続問題を解決する最も簡単な方法は、延長量を太さに応じた比率で動的に決めることでした。もし、より複雑で滑らかな方法を求める場合は、一般的にベジェ曲線と呼ばれるトランジションカーブ方式を利用することも検討できます。私はこの手法を高速道路交通工学の分野で学びましたが、簡単に説明すると以下のようなものです

ベジェ曲線

ベジェ曲線には複数の種類が存在することに注意してください。このアプローチを実際に採用する場合は、二次または三次のベジェ曲線方式を推奨します。特に挑戦的な実装を行う場合は、三次ベジェ曲線を検討できます。しかし、今回の実装ではシンプルな延長方式を採用します。全体として、角丸吹き出しを作成するという目的は達成できました。残る作業はシステムの動作確認であり、これは次のセクションで扱います。


バックテスト

テストを実施しました。以下は、コンパイル後の動作をまとめたGIFアニメーションです。

バックテストGIF


結論

結論として、MQL5においてベクターベースの長方形と三角形を組み合わせることで、上、下、左、右の各方向へポインタを向けられる、向き制御機能付きの角丸吹き出しを構築しました。また、アンチエイリアス処理された描画を実現するためにスーパーサンプリングを導入し、さらにサイズ、角丸半径、枠線、不透明度などを自由に調整できるようにすることで、動的なUI要素を作成できるようにしました。これらの角丸吹き出しと向き制御機能を利用することで、今後開発するトレーディングツールにおいて、より魅力的で高度なグラフィックコンポーネントを構築するための基盤が整いました。それでは、よいトレードを。

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/21271

EAのサンプル EAのサンプル
一般的なMACDを使ったEAを例として、MQL4開発の原則を紹介します。
流動性狩りの最適化:流動性狩りと市場構造転換の違いを見極める 流動性狩りの最適化:流動性狩りと市場構造転換の違いを見極める
流動性狩り後のトレードセットアップをどのように活用するかを明確に解説することを目的とした、トレンドフォロー型EAに関する記事です。本記事では、流動性狩りおよび流動性一掃をトレードのエントリー条件として最適に活用したいトレーダー向けに特別に設計されたEAについて詳しく解説します。また、流動性狩りと市場構造転換を正しく区別する方法、それぞれが発生した際にどのように検証し活用すべきかについても取り上げます。これにより、両者を混同することで生じる損失を軽減することを目指します。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
取引規律をコードに組み込む(第1回):MQL5でライブトレードに構造的な規律を実装する 取引規律をコードに組み込む(第1回):MQL5でライブトレードに構造的な規律を実装する
規律は意志力ではなく、システム設計によって信頼できるものになります。MQL5を用いてリアルタイム制約を実装し、取引回数の上限や日次エクイティに基づく停止条件によって取引行動を監視し、制約違反が発生した場合には自動的に必要なアクションを実行する仕組みを構築します。読者は、市場のプレッシャー下でも執行の安定性を保つためのガバナンスレイヤーを実装する実践的なテンプレートを習得できます。