English Русский Deutsch
preview
MQL5取引ツール(第17回):ベクトルベースの角丸長方形と三角形を探る

MQL5取引ツール(第17回):ベクトルベースの角丸長方形と三角形を探る

MetaTrader 5トレーディング |
29 1
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

第16回では、MetaQuotes Language 5 (MQL5)のcanvasベースのダッシュボードを拡張し、アンチエイリアス技術とスーパーサンプリングによる高解像度レンダリングを導入しました。その結果、より滑らかなグラフィック、境界線、各種UI要素を実現しました。第17回では、canvasを使用して角丸長方形と三角形を描画するためのベクトルベース手法について解説します。スーパーサンプリングを組み合わせることで、アンチエイリアス処理された滑らかな描画結果を実現します。 この方法は、幾何学的な事前計算、スキャンライン塗りつぶし、正確な境界線処理を扱うことで、今後のツール開発におけるモダンなcanvasオブジェクト作成の基盤となります。本記事では以下のトピックを扱います。

  1. ベクトルベースの角丸長方形と三角形の理解
  2. MQL5での実装
  3. バックテスト
  4. 結論

記事の最後には、滑らかな角丸図形を作成するための再利用可能な関数を利用できるようになります。これらは、今後の高度なUI要素へ統合するための基盤として使用できます。それでは詳しく見ていきましょう。


ベクトルベースの角丸長方形と三角形の理解

ベクトルベースによる角丸長方形と三角形の描画では、ピクセル単位のグリッドではなく、点、線、曲線などの数学的な図形表現を使用します。これにより、どのようなサイズでも鮮明さを維持できる、拡大縮小に依存しないグラフィックを実現できます。ラスタ方式では、拡大縮小時にエッジがギザギザになるエイリアシングが発生することがあります。一方、ベクトル方式では、円弧や接線の方程式を利用して正確な境界や塗りつぶし領域を計算します。このため、滑らかな表示によって操作性が向上するMQL5のUI要素に適しています。角丸は、鋭い頂点を円弧へ置き換えることで実現されます。この円弧の半径によって、曲率が制御されます。境界線については、オフセットされたパスや太さを持たせたエッジを使用して表現します。さらに、スーパーサンプリングによって高解像度で一度描画してから縮小処理をおこなうことで、不要な描画アーティファクトを除去し、さらに滑らかな結果を得られます。

今回の実装では、スーパーサンプリングを利用した高解像度canvasを構築し、三角形の円弧や接線に必要なジオメトリ計算を事前におこないます。また、両方の図形に対してスキャンライン塗りつぶしを使用し、内部領域を正確に描画します。さらに、ベクトルベースの辺と角部の円弧を利用して、カスタマイズ可能な境界線を追加します。サイズ、角の半径、透明度、色などのユーザー入力にも対応し、現代的なトレーディングインターフェースに適した柔軟でアンチエイリアス処理された図形を作成します。簡単にまとめると、今回の目的を視覚的に表現したものが以下の図になります。

角丸三角形と角丸長方形


MQL5での実装

MQL5でプログラムを作成するには、まずMetaEditorを開き、ナビゲーターで[Experts]フォルダを探します。[新規]タブをクリックして指示に従い、ファイルを作成します。ファイルが作成されたら、コーディング環境で、まずプログラム全体で使用する入力パラメータグローバル変数をいくつか宣言する必要があります。

//+------------------------------------------------------------------+
//|                           Rounded Rectangle & Triangle PART1.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

#include <Canvas\Canvas.mqh>

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "Position"
input int    shapesPositionX                  = 20;    // Shapes X position
input int    shapesPositionY                  = 20;    // Shapes Y position
input int    shapesGapPixels                  = 15;    // Gap between shapes (pixels)

input group "Rectangle"
input int    rectangleWidthPixels             = 250;   // Rectangle width
input int    rectangleHeightPixels            = 100;   // Rectangle height
input int    rectangleCornerRadiusPixels      = 5;     // Rectangle corner radius
input bool   rectangleShowBorder              = true;  // Show rectangle border
input int    rectangleBorderThicknessPixels   = 1;     // Rectangle border thickness
input color  rectangleBorderColor             = clrBlue; // Rectangle border color
input int    rectangleBorderOpacityPercent    = 80;      // Rectangle border opacity (0-100%)
input color  rectangleBackgroundColor         = clrBlue; // Rectangle background color
input int    rectangleBackgroundOpacityPercent= 30;      // Rectangle background opacity (0-100%)

input group "Triangle"
input int    triangleBaseWidthPixels          = 250;     // Triangle base width (pixels)
input double triangleHeightAsPercentOfWidth   = 86.6;    // Height as % of width (86.6=equilateral, <86.6=flat, >86.6=tall)
input int    triangleCornerRadiusPixels       = 12;      // Triangle corner radius
input bool   triangleShowBorder               = true;    // Show triangle border
input int    triangleBorderThicknessPixels    = 1;       // Triangle border thickness
input color  triangleBorderColor              = clrRed;  // Triangle border color
input int    triangleBorderOpacityPercent     = 80;      // Triangle border opacity (0-100%)
input color  triangleBackgroundColor          = clrRed;  // Triangle background color
input int    triangleBackgroundOpacityPercent = 30;      // Triangle background opacity (0-100%)

input group "General"
input int    supersamplingLevel               = 4;       // Supersampling level (1=off, 2=2x, 4=4x)

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CCanvas rectangleCanvas, rectangleHighResCanvas;                //--- Declare rectangle canvas objects
CCanvas triangleCanvas,  triangleHighResCanvas;                 //--- Declare triangle canvas objects
string  rectangleCanvasName = "RoundedRectCanvas";              //--- Set rectangle canvas name
string  triangleCanvasName  = "RoundedTriCanvas";               //--- Set triangle canvas name
int     supersamplingFactor;                                    //--- Store supersampling factor
int     computedTriangleHeightPixels;                           //--- Store computed triangle height in pixels
double triangleSharpVerticesX[3], triangleSharpVerticesY[3];    //--- Store sharp vertices for triangle
double triangleArcCentersX[3], triangleArcCentersY[3];          //--- Store arc centers for triangle
double triangleTangentPointsX[3][2], triangleTangentPointsY[3][2]; //--- Store tangent points for triangle
double triangleArcStartAngles[3], triangleArcEndAngles[3];      //--- Store arc sweep angles in radians
int    triangleHighResWidth, triangleHighResHeight;             //--- Store high-res dimensions for triangle

実装は、まず Canvasライブラリをマクロ「#include <Canvas\Canvas.mqh>」でインクルードすることから始めます。このライブラリには、MQL5でグラフィックcanvasを作成および操作するために必要なクラスやメソッドが含まれています。これにより、チャート上へ長方形や三角形などのカスタム図形を直接描画できます。

次に、設定を管理しやすくするため、ユーザー入力を論理的なグループへ整理します。Positionグループでは、図形のX座標とY座標、および図形間の間隔を設定します。Rectangleグループでは、幅、高さ、角の半径を定義します。また、境界線に関する設定として、表示の有無、太さ、色、透明度を指定できます。さらに、背景色とその透明度も設定します。Triangleグループでは、同様に三角形の底辺幅、高さ、角の半径、境界線設定、背景設定を指定します。高さは幅に対する割合として設定され、正三角形に近い比率となるよう、初期値は86.6に設定されています。Generalグループでは、アンチエイリアス品質を制御するスーパーサンプリングレベルを設定します。値が1の場合はスーパーサンプリングなしとなり、4などの高い値を設定すると、より滑らかな描画結果を得られます。

描画処理をサポートするため、通常解像度版と高解像度版の長方形および三角形用canvasオブジェクトをグローバル変数として宣言します。これらには、識別しやすいようにRoundedRectCanvasやRoundedTriCanvasなどの名前を設定します。最後に、スーパーサンプリング倍率を保持する変数、計算された三角形の高さ、そして三角形のジオメトリ情報を格納する配列を準備します。これらの配列には、鋭角の頂点、円弧の中心点、接点(三角形の3つの角それぞれについて2点分を保持する接点配列[3][2])、ラジアン単位の開始角度と終了角度、さらに三角形canvas用の高解像度サイズなどが含まれます。グローバル変数の準備が完了したため、次にプログラム全体で使用するヘルパー関数を定義していきます。

//+------------------------------------------------------------------+
//| Shared Utilities                                                 |
//+------------------------------------------------------------------+
uint ColorToARGBWithOpacity(color clr, int opacityPercent) {
   uchar redComponent = (uchar)((clr >> 0)  & 0xFF);             //--- Extract red component
   uchar greenComponent = (uchar)((clr >> 8)  & 0xFF);           //--- Extract green component
   uchar blueComponent = (uchar)((clr >> 16) & 0xFF);            //--- Extract blue component
   uchar alphaComponent = (uchar)((opacityPercent * 255) / 100); //--- Calculate alpha component from opacity
   return ((uint)alphaComponent << 24) | ((uint)redComponent << 16) | ((uint)greenComponent << 8) | (uint)blueComponent; //--- Combine into ARGB value
}

void BicubicDownsample(CCanvas &targetCanvas, CCanvas &highResCanvas) {
   int targetWidth  = targetCanvas.Width();                      //--- Get target canvas width
   int targetHeight = targetCanvas.Height();                     //--- Get target canvas height

   for(int pixelY = 0; pixelY < targetHeight; pixelY++) {        //--- Loop over target height pixels
      for(int pixelX = 0; pixelX < targetWidth; pixelX++) {      //--- Loop over target width pixels
         double sourceX = pixelX * supersamplingFactor;          //--- Calculate source X position
         double sourceY = pixelY * supersamplingFactor;          //--- Calculate source Y position

         double sumAlpha = 0, sumRed = 0, sumGreen = 0, sumBlue = 0; //--- Initialize sum variables
         double weightSum = 0;                                   //--- Initialize weight sum

         for(int deltaY = 0; deltaY < supersamplingFactor; deltaY++) { //--- Loop over delta Y for supersampling
            for(int deltaX = 0; deltaX < supersamplingFactor; deltaX++) { //--- Loop over delta X for supersampling
               int sourcePixelX = (int)(sourceX + deltaX);       //--- Compute source pixel X
               int sourcePixelY = (int)(sourceY + deltaY);       //--- Compute source pixel Y

               if(sourcePixelX >= 0 && sourcePixelX < highResCanvas.Width() && sourcePixelY >= 0 && sourcePixelY < highResCanvas.Height()) { //--- Check if within high-res bounds
                  uint pixelValue = highResCanvas.PixelGet(sourcePixelX, sourcePixelY); //--- Get pixel value from high-res canvas

                  uchar alpha = (uchar)((pixelValue >> 24) & 0xFF); //--- Extract alpha component
                  uchar red = (uchar)((pixelValue >> 16) & 0xFF);   //--- Extract red component
                  uchar green = (uchar)((pixelValue >> 8)  & 0xFF); //--- Extract green component
                  uchar blue = (uchar)(pixelValue         & 0xFF);  //--- Extract blue component

                  double weight = 1.0;                           //--- Set weight to 1.0
                  sumAlpha += alpha * weight;                    //--- Accumulate weighted alpha
                  sumRed += red * weight;                        //--- Accumulate weighted red
                  sumGreen += green * weight;                    //--- Accumulate weighted green
                  sumBlue += blue * weight;                      //--- Accumulate weighted blue
                  weightSum += weight;                           //--- Accumulate total weight
               }
            }
         }

         if(weightSum > 0) {                                       //--- Check if weight sum is positive
            uchar finalAlpha = (uchar)(sumAlpha / weightSum);      //--- Compute final alpha
            uchar finalRed = (uchar)(sumRed / weightSum);          //--- Compute final red
            uchar finalGreen = (uchar)(sumGreen / weightSum);      //--- Compute final green
            uchar finalBlue = (uchar)(sumBlue / weightSum);        //--- Compute final blue

            uint finalColor = ((uint)finalAlpha << 24) | ((uint)finalRed << 16) |
                              ((uint)finalGreen << 8)  | (uint)finalBlue; //--- Combine into final color
            targetCanvas.PixelSet(pixelX, pixelY, finalColor);     //--- Set pixel on target canvas
         }
      }
   }
}

double NormalizeAngle(double angle) {
   double twoPi = 2.0 * M_PI;                                   //--- Define two pi constant
   angle = MathMod(angle, twoPi);                               //--- Modulo angle by two pi
   if(angle < 0) angle += twoPi;                                //--- Adjust if angle is negative
   return angle;                                                //--- Return normalized angle
}

bool IsAngleBetween(double angle, double startAngle, double endAngle) {
   angle = NormalizeAngle(angle);                               //--- Normalize angle
   startAngle = NormalizeAngle(startAngle);                     //--- Normalize start angle
   endAngle = NormalizeAngle(endAngle);                         //--- Normalize end angle
   
   double span = NormalizeAngle(endAngle - startAngle);         //--- Compute span
   double relativeAngle = NormalizeAngle(angle - startAngle);   //--- Compute relative angle
   
   return relativeAngle <= span;                                //--- Return if within span
}

まず、ColorToARGBWithOpacity関数を作成します。この関数は、指定された透明度を適用しながら、カラー値をARGB形式へ変換する役割を持ちます。ビットシフトを使用して赤、緑、青の各成分を取得し、透明度を0〜255の範囲へ変換してアルファチャンネルを計算します。その後、これらの値を1つのuint値へ統合します。これにより、図形の塗りつぶしや境界線に透明度を設定できるようになります。次に、BicubicDownsample関数を実装します。この関数は、高解像度canvasから通常解像度canvasへ縮小する際にアンチエイリアス処理をおこなうために使用されます。まず、描画先となるcanvasのサイズを取得します。その後、各ピクセルを順番に処理し、スーパーサンプリングされた元画像内の対応領域へマッピングします。各サブピクセルからARGB成分の加重合計を取得し、平均化処理をおこないます。この実装では、各サンプルに均一な重みを適用します。サンプルが存在する場合は、最終的なARGB値を計算してピクセルへ設定します。この処理によって、高解像度で描画された細かな情報が滑らかに混合され、図形のエッジがより自然に表示されます。

角度計算を一貫して処理するため、次にNormalizeAngle関数を定義します。この関数では、2πを基準として角度を正規化し、負の値を調整することで、すべての角度を0〜2πの範囲へ収めます。これにより、円弧描画時に安定して角度を比較できるようになります。続いて、IsAngleBetween関数を追加します。この関数は、指定した角度が開始角度と終了角度の範囲内に存在するかを判定します。入力された角度を正規化した後、角度範囲の幅と相対位置を計算し、対象角度が範囲内に含まれる場合にtrueを返します。この処理は、曲線境界を描画する際に、不要な重複描画や隙間を防ぎながら、正確にピクセルを含めるために重要です。これらの角度関連処理に加えて、次に四角形領域を塗りつぶすための関数も必要になります。

void FillQuadrilateral(CCanvas &canvas, double &verticesX[], double &verticesY[], uint fillColor) {
   double minY = verticesY[0], maxY = verticesY[0];             //--- Initialize min and max Y
   for(int i = 1; i < 4; i++) {                                 //--- Loop over vertices
      if(verticesY[i] < minY) minY = verticesY[i];              //--- Update min Y
      if(verticesY[i] > maxY) maxY = verticesY[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[8];                                 //--- Declare intersections array
      int intersectionCount = 0;                                //--- Initialize intersection count

      for(int i = 0; i < 4; i++) {                              //--- Loop over edges
         int nextIndex = (i + 1) % 4;                           //--- Get next index
         double x0 = verticesX[i],    y0 = verticesY[i];        //--- Get start coordinates
         double x1 = verticesX[nextIndex], y1 = verticesY[nextIndex]; //--- Get end coordinates

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

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

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

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

      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
            canvas.PixelSet(x, y, fillColor);                   //--- Set pixel with fill color
      }
   }
}

次に、FillQuadrilateral関数を実装します。この関数は、スキャンラインアルゴリズムを使用してcanvas上へ塗りつぶされた四角形を描画します。この方法を使用することで、組み込み描画機能に依存することなく、境界線や図形本体などを正確にベクトルベースで塗りつぶすことができます。処理の最初では、入力された頂点配列verticesYから最小Y座標と最大Y座標を取得し、描画対象となる垂直方向の範囲を決定します。その後、完全なピクセル範囲をカバーするため、ceilとfloorを使用して開始および終了スキャンラインの整数値を計算します。各スキャンラインyでは、サブピクセル精度を高めるために0.5ピクセル分のオフセットを加えたscanlineYを使用します。これにより、アンチエイリアス処理時の精度が向上します。続いて、4辺それぞれに対して線形補間をおこない、最大8個までのX方向の交点を取得します。この処理では、スキャンラインが各辺と垂直方向で交差する場合のみ計算します。水平な辺や範囲外となる補間係数については除外されます。

取得した交点は、小規模な配列であるためバブルソートを使用して昇順に並べ替えます。その後、交点を2つずつ組み合わせ、左右のX座標の間を水平方向に塗りつぶします。塗りつぶしにはfillColorを使用し、PixelSetメソッドによって各ピクセルへ色を設定します。この処理は、高解像度canvas上で非凸形状や不規則な四角形を扱う際に重要です。ピクセル単位で正確な塗りつぶし計算をおこなうことで、角丸図形の境界線を形成する太いエッジ部分を、重複や隙間なく滑らかに描画できます。

ここで、スキャンラインアルゴリズムについて簡単に説明します。このアルゴリズムは個々のピクセル単位で処理するのではなく、画像を走査線(水平ライン)ごとに順に処理します。各スキャンライン上で図形の境界と交差する点をすべて記録し、その交点のペアの間を塗りつぶすことでポリゴン内部を描画します。

紙の上に図形を描く場面を想像すると分かりやすいでしょう。ペンを使って左端から右方向へ一直線に線を引いていくと、図形の境界に到達した時点で塗りつぶしを開始し、次の境界で終了します。スキャンラインアルゴリズムも同じ考え方で動作します。以下の図では、この動作を視覚的に示しています。赤い点はポリゴンの頂点、青い点はスキャンラインとポリゴンとの交点を表しています。

スキャンラインアルゴリズム

これで必要な関数が準備できたため、次にこれらを利用して角丸図形を作成します。まずは長方形から実装を開始します。ただし、コードを整理し、再利用性と保守性を高めるために、ここでもいくつかのヘルパー関数を用意する必要があります。

void FillRoundedRectangleHiRes(int positionX, int positionY, int width, int height, int radius, uint fillColor) {
   rectangleHighResCanvas.FillRectangle(positionX + radius, positionY,          positionX + width - radius, positionY + height,          fillColor); //--- Fill central rectangle
   rectangleHighResCanvas.FillRectangle(positionX,          positionY + radius, positionX + radius,         positionY + height - radius, fillColor); //--- Fill left strip
   rectangleHighResCanvas.FillRectangle(positionX + width - radius, positionY + radius, positionX + width, positionY + height - radius, fillColor); //--- Fill right strip

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

void FillCircleQuadrant(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
            rectangleHighResCanvas.PixelSet(centerX + deltaX, centerY + deltaY, fillColor); //--- Set pixel
      }
   }
}

void DrawRoundedRectangleBorderHiRes(int positionX, int positionY, int width, int height, int radius, uint borderColorARGB) {
   int scaledThickness = rectangleBorderThicknessPixels * supersamplingFactor; //--- Scale border thickness

   DrawRectStraightEdge(positionX + radius, positionY, positionX + width - radius, positionY, scaledThickness, borderColorARGB); //--- Draw top edge
   DrawRectStraightEdge(positionX + width - radius, positionY + height - 1, positionX + radius, positionY + height - 1, scaledThickness, borderColorARGB); //--- Draw bottom edge
   DrawRectStraightEdge(positionX, positionY + height - radius, positionX, positionY + radius, scaledThickness, borderColorARGB); //--- Draw left edge
   DrawRectStraightEdge(positionX + width - 1, positionY + radius, positionX + width - 1, positionY + height - radius, scaledThickness, borderColorARGB); //--- Draw right edge

   DrawRectCornerArcPrecise(positionX + radius, positionY + radius, radius, scaledThickness, borderColorARGB, 
                            M_PI, M_PI * 1.5);                  //--- Draw top-left arc
   DrawRectCornerArcPrecise(positionX + width - radius, positionY + radius, radius, scaledThickness, borderColorARGB,
                            M_PI * 1.5, M_PI * 2.0);            //--- Draw top-right arc
   DrawRectCornerArcPrecise(positionX + radius, positionY + height - radius, radius, scaledThickness, borderColorARGB,
                            M_PI * 0.5, M_PI);                  //--- Draw bottom-left arc
   DrawRectCornerArcPrecise(positionX + width - radius, positionY + height - radius, radius, scaledThickness, borderColorARGB,
                            0.0, M_PI * 0.5);                   //--- Draw bottom-right arc
}

void DrawRectStraightEdge(double startX, double startY, double endX, double endY, int thickness, uint borderColor) {
   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 = 1.5;                                //--- 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(rectangleHighResCanvas, verticesX, verticesY, borderColor); //--- Fill quadrilateral for edge
}

void DrawRectCornerArcPrecise(int centerX, int centerY, int radius, int thickness, uint borderColor,
                              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
            rectangleHighResCanvas.PixelSet(centerX + deltaX, centerY + deltaY, borderColor); //--- Set pixel
      }
   }
}

まず、FillRoundedRectangleHiRes関数を実装し、高解像度canvas上へ角丸長方形の塗りつぶし部分を描画します。最初に、角部分を除いた中央の長方形領域を塗りつぶします。次に、左右へ縦方向のストリップを追加し、直線部分を接続します。この方法により、重複や隙間が発生することなく、図形全体を滑らかに覆うことができます。最後に、4つの角丸部分を完成させるため、各象限に対してFillCircleQuadrant関数を呼び出します。この関数には、円の中心座標、半径、塗りつぶし色、そして象限番号を渡します。象限番号は、右上を1、左上を2、左下を3、右下を4として指定します。FillCircleQuadrantでは、少し広めに設定したピクセル範囲を走査し、距離計算によって対象点が指定された象限内かつ円の半径内に存在するかを確認します。条件を満たすピクセルには色を設定します。これにより、直線部分と自然につながる、正確な4分円形の角丸領域を描画できます。

次に、境界線を処理するためにDrawRoundedRectangleBorderHiRes関数を作成します。この関数では、スーパーサンプリング倍率に合わせて境界線の太さを拡大し、以下の4つの辺をDrawRectStraightEdgeによって描画します。上辺、下辺、左辺、右辺をそれぞれ処理します。また、角部分についてはDrawRectCornerArcPreciseを使用して円弧を描画します。円弧の範囲はラジアン単位で指定され、例えば左上の角ではπから1.5πまでの範囲を使用します。これにより、すべての角で一貫した曲率とアンチエイリアス処理されたエッジを実現できます。DrawRectStraightEdgeでは、始点から終点までのベクトル方向と、それに垂直な方向ベクトルを計算します。その後、角との接続を滑らかにするためにラインを少し延長し、半分の太さ分だけオフセットした四角形ストリップを定義します。この領域は、先ほど作成した四角形塗りつぶし関数を使用して描画されます。これにより、円弧部分と正確に接続する、太く滑らかな直線境界線が形成されます。

最後に、DrawRectCornerArcPrecise関数では、内側半径と外側半径の間に存在するリング状の領域を描画します。この処理では各ピクセルを走査し、距離条件とIsAngleBetween関数による角度条件を確認します。指定された円弧範囲内にあるピクセルのみへ境界線カラーを設定します。この処理は、拡大縮小された高解像度レンダリングにおいて、ギザギザのない高品質な曲線境界を実現するために重要です。これらの関数が完成したことで、角丸長方形を描画する準備が整いました。

//+------------------------------------------------------------------+
//| Rounded Rectangle                                                |
//+------------------------------------------------------------------+
void DrawRoundedRectangle() {
   int positionX      = 10 * supersamplingFactor;                     //--- Set X position scaled
   int positionY      = 10 * supersamplingFactor;                     //--- Set Y position scaled
   int scaledWidth  = rectangleWidthPixels  * supersamplingFactor;    //--- Scale width
   int scaledHeight = rectangleHeightPixels * supersamplingFactor;    //--- Scale height
   int scaledRadius = rectangleCornerRadiusPixels * supersamplingFactor; //--- Scale radius

   uint backgroundColorARGB     = ColorToARGBWithOpacity(rectangleBackgroundColor,     rectangleBackgroundOpacityPercent); //--- Get background ARGB
   uint borderColorARGB = ColorToARGBWithOpacity(rectangleBorderColor, rectangleBorderOpacityPercent); //--- Get border ARGB

   FillRoundedRectangleHiRes(positionX, positionY, scaledWidth, scaledHeight, scaledRadius, backgroundColorARGB); //--- Fill high-res rectangle

   if(rectangleShowBorder && rectangleBorderThicknessPixels > 0)      //--- Check if border should be shown
      DrawRoundedRectangleBorderHiRes(positionX, positionY, scaledWidth, scaledHeight, scaledRadius, borderColorARGB); //--- Draw border on high-res

   BicubicDownsample(rectangleCanvas, rectangleHighResCanvas);        //--- Downsample to display canvas

   rectangleCanvas.FontSet("Arial", 13, FW_NORMAL);                   //--- Set font for text
   string displayText = "Rounded Rectangle";                          //--- Set display text
   int textWidth, textHeight;                                         //--- Declare text dimensions
   rectangleCanvas.TextSize(displayText, textWidth, textHeight);      //--- Get text size
   int textPositionX = 10 + (rectangleWidthPixels  - textWidth)  / 2; //--- Compute text X position
   int textPositionY = 10 + (rectangleHeightPixels - textHeight) / 2; //--- Compute text Y position
   rectangleCanvas.TextOut(textPositionX, textPositionY, displayText, (uint)0xFF000000, TA_LEFT); //--- Draw text on canvas
}

ここでは、canvas上へ角丸長方形を描画する処理全体を管理するDrawRoundedRectangle関数を定義します。最初に、スーパーサンプリング倍率を使用して、描画位置のオフセット、幅、高さ、角の半径をスケーリングします。これにより、高解像度での正確な描画処理が可能となり、アンチエイリアス処理に適した状態を準備できます。次に、背景色と境界線色をARGB形式へ変換します。この変換では、ColorToARGBWithOpacity関数を使用して透明度を適用します。これにより、完全な不透明表示ではなく、半透明効果を持たせた描画が可能となり、UI要素に視覚的な奥行きを与えられます。図形を構築するため、スケーリング済みのパラメータと背景色を使用してFillRoundedRectangleHiResを呼び出し、高解像度canvas上へ内部領域を塗りつぶします。その後、rectangleShowBorderが有効で、かつ境界線の太さが正の値である場合は、DrawRoundedRectangleBorderHiResを実行して、指定した境界線カラーでアウトラインを追加します。

次に、高解像度canvasから通常解像度canvasへBicubicDownsampleを使用して縮小処理をおこないます。この処理では、高解像度で描画された細かな情報を滑らかに混合し、最終的な表示品質を向上させます。最後に、通常解像度canvas上へラベルを描画します。FontSetを使用して、フォントをArial、サイズ13、太さFW_NORMALに設定します。続いて、TextSizeによって「Rounded Rectangle」という文字列のサイズを取得し、中央配置となる座標を計算します。その後、TextOutを使用して、左寄せ設定かつ不透明な黒色(0xFF000000)でテキストを描画します。 このラベルによって、表示されている図形の内容が明確になり、視覚的な説明要素として機能します。これで角丸長方形の描画処理が完成したため、次に初期化イベントハンドラーからこの関数を呼び出して描画を実行できます。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   supersamplingFactor = supersamplingLevel;                    //--- Assign supersampling factor from input
   if(supersamplingFactor < 1) {                                //--- Check if supersampling factor is less than 1
      Print("Warning: supersamplingLevel must be at least 1. Setting to 1."); //--- Print warning message
      supersamplingFactor = 1;                                  //--- Set supersampling factor to minimum value
   }


   int rectangleCanvasWidth = rectangleWidthPixels  + 40;       //--- Compute rectangle canvas width with padding
   int rectangleCanvasHeight = rectangleHeightPixels + 40;      //--- Compute rectangle canvas height with padding
   int rectanglePositionY       = shapesPositionY;              //--- Set rectangle Y position

   if(!rectangleCanvas.CreateBitmapLabel(0, 0, rectangleCanvasName, shapesPositionX, rectanglePositionY,
                                    rectangleCanvasWidth, rectangleCanvasHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create rectangle canvas bitmap label
      Print("Error creating rectangle canvas: ", GetLastError()); //--- Print error message if creation fails
      return(INIT_FAILED);                                        //--- Return initialization failure
   }

   if(!rectangleHighResCanvas.Create(rectangleCanvasName + "_hires",
                        rectangleCanvasWidth * supersamplingFactor,
                        rectangleCanvasHeight * supersamplingFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create high-res rectangle canvas
      Print("Error creating rectangle hi-res canvas: ", GetLastError()); //--- Print error message if creation fails
      return(INIT_FAILED);                                        //--- Return initialization failure
   }


   rectangleCanvas.Erase(ColorToARGB(clrNONE, 0));                //--- Clear rectangle canvas
   rectangleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));         //--- Clear high-res rectangle canvas

   DrawRoundedRectangle();                                        //--- Draw rounded rectangle

   rectangleCanvas.Update();                                      //--- Update rectangle canvas display

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

OnInitイベントハンドラでは、プログラムの初期化処理をおこないます。まず、ユーザー入力で設定されたsupersamplingLevelの値をグローバル変数supersamplingFactorへ代入します。その後、この値が1未満であるかを確認し、条件を満たす場合は最小値へリセットします。同時に、正しいアンチエイリアス処理を維持するため、警告メッセージを出力します。次に、入力された幅と高さへ余白分を追加し、長方形canvasのサイズを計算します。Y座標にはshapesPositionYの値を使用します。続いて、CreateBitmapLabelを使用して通常解像度canvasを作成します。この関数には、チャートID、サブウィンドウ番号、canvas名、配置位置、サイズ、そして透明度をサポートするためのCOLOR_FORMAT_ARGB_NORMALIZE形式を指定します。canvas作成に失敗した場合は、GetLastErrorによってエラー情報を取得して出力し、INIT_FAILEDを返して初期化を停止します。

次に、Createで高解像度canvasを作成します。通常canvasとは異なる識別用の名前を付け、幅と高さをsupersamplingFactor倍したサイズで作成します。ここでも同じカラー形式を使用し、作成に失敗した場合は同様にエラー処理をおこないます。描画準備として、両方のcanvasをErase関数でクリアします。この際、clrNONEから生成した透明なARGBカラーを渡し、既存の描画内容を消去します。準備が完了した後、DrawRoundedRectangle関数を呼び出して実際の角丸長方形描画を実行します。最後に、通常解像度canvasの表示をUpdateによって更新し、INIT_SUCCEEDEDを返して正常な初期化完了を通知します。コンパイルすると、次の結果が得られます。

角丸長方形

表示結果から、角丸長方形が正常に描画されていることを確認できます。次に残されている作業は、同様のアプローチを使用して角丸三角形を描画することです。これを実現するために、次のロジックを使用します。まず、三角形のジオメトリ情報を事前計算するためのヘルパー関数から作成します。

//+------------------------------------------------------------------+
//| Rounded Triangle                                                 |
//+------------------------------------------------------------------+
void PrecomputeTriangleGeometry() {
   int scalingFactor = supersamplingFactor;                     //--- Set scaling factor

   double basePositionX = 10.0 * scalingFactor;                 //--- Set base X position scaled
   double basePositionY = 10.0 * scalingFactor;                 //--- Set base Y position scaled
   double baseWidth = (double)triangleBaseWidthPixels   * scalingFactor; //--- Scale base width
   double baseHeight = (double)computedTriangleHeightPixels * scalingFactor; //--- Scale base height

   triangleSharpVerticesX[0] = basePositionX + baseWidth / 2.0; triangleSharpVerticesY[0] = basePositionY; //--- Set top vertex
   triangleSharpVerticesX[1] = basePositionX;               triangleSharpVerticesY[1] = basePositionY + baseHeight; //--- Set bottom-left vertex
   triangleSharpVerticesX[2] = basePositionX + baseWidth;          triangleSharpVerticesY[2] = basePositionY + baseHeight; //--- Set bottom-right vertex

   double scaledRadius = (double)triangleCornerRadiusPixels * scalingFactor; //--- Scale radius

   for(int cornerIndex = 0; cornerIndex < 3; cornerIndex++) {   //--- Loop over corners
      int previousIndex = (cornerIndex + 2) % 3;                //--- Get previous index
      int nextIndex = (cornerIndex + 1) % 3;                    //--- Get next index

      double edgeA_X = triangleSharpVerticesX[cornerIndex] - triangleSharpVerticesX[previousIndex],  edgeA_Y = triangleSharpVerticesY[cornerIndex] - triangleSharpVerticesY[previousIndex]; //--- Compute edge A vector
      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 = triangleSharpVerticesX[nextIndex] - triangleSharpVerticesX[cornerIndex],  edgeB_Y = triangleSharpVerticesY[nextIndex] - triangleSharpVerticesY[cornerIndex]; //--- Compute edge B vector
      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
      triangleArcCentersX[cornerIndex] = triangleSharpVerticesX[cornerIndex] + bisectorX * distanceToCenter; //--- Set arc center X
      triangleArcCentersY[cornerIndex] = triangleSharpVerticesY[cornerIndex] + bisectorY * distanceToCenter; //--- Set arc center Y

      double deltaX_A = triangleSharpVerticesX[cornerIndex] - triangleSharpVerticesX[previousIndex],  deltaY_A = triangleSharpVerticesY[cornerIndex] - triangleSharpVerticesY[previousIndex]; //--- Compute delta A
      double lengthSquared_A = deltaX_A*deltaX_A + deltaY_A*deltaY_A; //--- Compute length squared A
      double interpolationFactor_A = ((triangleArcCentersX[cornerIndex] - triangleSharpVerticesX[previousIndex])*deltaX_A + (triangleArcCentersY[cornerIndex] - triangleSharpVerticesY[previousIndex])*deltaY_A) / lengthSquared_A; //--- Compute factor A
      triangleTangentPointsX[cornerIndex][1] = triangleSharpVerticesX[previousIndex] + interpolationFactor_A * deltaX_A; //--- Set tangent point X arriving
      triangleTangentPointsY[cornerIndex][1] = triangleSharpVerticesY[previousIndex] + interpolationFactor_A * deltaY_A; //--- Set tangent point Y arriving

      double deltaX_B = triangleSharpVerticesX[nextIndex] - triangleSharpVerticesX[cornerIndex],  deltaY_B = triangleSharpVerticesY[nextIndex] - triangleSharpVerticesY[cornerIndex]; //--- Compute delta B
      double lengthSquared_B = deltaX_B*deltaX_B + deltaY_B*deltaY_B; //--- Compute length squared B
      double interpolationFactor_B = ((triangleArcCentersX[cornerIndex] - triangleSharpVerticesX[cornerIndex])*deltaX_B + (triangleArcCentersY[cornerIndex] - triangleSharpVerticesY[cornerIndex])*deltaY_B) / lengthSquared_B; //--- Compute factor B
      triangleTangentPointsX[cornerIndex][0] = triangleSharpVerticesX[cornerIndex] + interpolationFactor_B * deltaX_B; //--- Set tangent point X leaving
      triangleTangentPointsY[cornerIndex][0] = triangleSharpVerticesY[cornerIndex] + interpolationFactor_B * deltaY_B; //--- Set tangent point Y leaving

      triangleArcStartAngles[cornerIndex] = MathArctan2(triangleTangentPointsY[cornerIndex][1] - triangleArcCentersY[cornerIndex], triangleTangentPointsX[cornerIndex][1] - triangleArcCentersX[cornerIndex]); //--- Set start angle
      triangleArcEndAngles[cornerIndex]   = MathArctan2(triangleTangentPointsY[cornerIndex][0] - triangleArcCentersY[cornerIndex], triangleTangentPointsX[cornerIndex][0] - triangleArcCentersX[cornerIndex]); //--- Set end angle
   }
}

bool AngleInArcSweep(int cornerIndex, double angle) {
   double twoPi  = 2.0 * M_PI;                                  //--- Define two pi constant
   double startAngleMod  = MathMod(triangleArcStartAngles[cornerIndex] + twoPi, twoPi); //--- Modulo start angle
   double endAngleMod  = MathMod(triangleArcEndAngles[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
   }
}

まず、PrecomputeTriangleGeometry関数を作成し、高解像度canvas上で角丸三角形を描画するために必要なジオメトリ情報を事前計算します。最初に、スーパーサンプリング倍率をローカル変数へ格納します。その後、入力値から取得した基準位置、幅、高さを高解像度用にスケーリングし、元の比率を維持した状態で描画できるようにします。次に、3つの鋭角の頂点を定義します。1つ目は上側の頂点で、基準X座標に幅の中心位置を加えた位置に配置します。2つ目は左下の頂点で、基準X座標と高さを加えたY座標によって定義します。3つ目は右下の頂点で、基準X座標に幅を加え、左下と同じ高さへ配置します。続いて、角丸半径をスケーリングし、3つの角を順番に処理します。ループ変数cornerIndexを使用し、各角について処理をおこないます。前後の頂点インデックスは、3で割った余りを利用して循環的に取得します。これにより、三角形の頂点を順番に処理できます。次に、前の頂点から現在の頂点へ向かう辺ベクトルAと、現在の頂点から次の頂点へ向かう辺ベクトルBを求めます。その後、これらのベクトルを正規化し、90度回転によって外向き法線を計算します。さらに、2つの法線を加算して角の二等分方向を求め、正規化します。もしベクトル長が極端に小さい場合は、ゼロ除算を防ぐため、片方の法線を代替として使用します。

円弧中心を配置するために、まず内角の余弦値を計算します。これは、反転させた辺ベクトルの内積を利用して求め、値が有効範囲を超えないように制限します。続いて、半角とその正弦値を計算します。正弦値がゼロに近い場合は最小値を設定し、計算エラーを防ぎます。その後、半径を正弦値で割って二等分線方向への距離を求め、頂点からオフセットすることで円弧中心を決定します。次に、円弧中心を隣接する各辺へ投影し、接点を計算します。到達側の辺Aについてはベクトル投影を使用し、接線配列のインデックス1へ保存します。離脱側の辺Bについてはインデックス0へ保存します。これにより、直線部分と円弧部分の境界が滑らかにつながります。最後に、各円弧の開始角度と終了角度を設定します。これは、円弧中心から接点までのオフセットを使用し、MathArctan2によって計算します。この角度範囲は、後続の塗りつぶし処理や境界線描画時に、各ピクセルが円弧範囲内に存在するかを判定するために使用されます。この事前計算により、変形や歪みを発生させることなく、正確なベクトルベースの角丸処理が可能になります。

次に、AngleInArcSweep関数を作成します。この関数では、開始角度、終了角度、判定対象角度をMathModを用いて0〜2πの範囲へ正規化します。その後、反時計回り方向の角度範囲を計算します。範囲がπ以下の場合は短い円弧として扱い、開始角度からの相対位置を確認します。一方、範囲がπを超える場合は時計回り方向の範囲を使用し、終了角度側から判定します。また、浮動小数点計算による誤差を考慮するため、小さなイプシロン値を追加します。これにより、円弧の方向に関係なく、指定された点の角度が正しい円弧範囲内に存在するかを安定して判定できます。次は、パラメトリック計算をおこなうヘルパー関数を作成します。

void FillRoundedTriangleHiRes(uint fillColor) {
   double minY = triangleSharpVerticesY[0], maxY = triangleSharpVerticesY[0]; //--- Initialize min and max Y
   for(int i = 1; i < 3; i++) {                                 //--- Loop over vertices
      if(triangleSharpVerticesY[i] < minY) minY = triangleSharpVerticesY[i]; //--- Update min Y
      if(triangleSharpVerticesY[i] > maxY) maxY = triangleSharpVerticesY[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 straight edges
         int nextIndex = (edgeIndex + 1) % 3;                   //--- Get next index
         double startX = triangleTangentPointsX[edgeIndex][0],    startY = triangleTangentPointsY[edgeIndex][0]; //--- Get start tangent
         double endX = triangleTangentPointsX[nextIndex][1], endY = triangleTangentPointsY[nextIndex][1]; //--- Get end 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
      }

      for(int cornerIndex = 0; cornerIndex < 3; cornerIndex++) { //--- Loop over corner arcs
         double centerX = triangleArcCentersX[cornerIndex],  centerY = triangleArcCentersY[cornerIndex]; //--- Get arc center
         double radius  = (double)triangleCornerRadiusPixels * supersamplingFactor; //--- Get scaled radius
         double deltaY = scanlineY - centerY;                   //--- Compute delta Y

         if(MathAbs(deltaY) > radius) continue;                 //--- Skip if outside 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(AngleInArcSweep(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
            triangleHighResCanvas.PixelSet(x, y, fillColor);     //--- Set pixel with fill color
      }
   }
}

void DrawRoundedTriangleBorderHiRes(uint borderColor) {
   int scaledThickness = triangleBorderThicknessPixels * supersamplingFactor; //--- Scale border thickness

   for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) {         //--- Loop over edges
      int nextIndex = (edgeIndex + 1) % 3;                      //--- Get next index
      double startX = triangleTangentPointsX[edgeIndex][0],    startY = triangleTangentPointsY[edgeIndex][0]; //--- Get start tangent
      double endX = triangleTangentPointsX[nextIndex][1], endY = triangleTangentPointsY[nextIndex][1]; //--- Get end tangent

      DrawTriStraightEdge(startX, startY, endX, endY, scaledThickness, borderColor); //--- Draw straight edge
   }

   for(int cornerIndex = 0; cornerIndex < 3; cornerIndex++)     //--- Loop over corners
      DrawTriCornerArcPrecise(cornerIndex, scaledThickness, borderColor); //--- Draw corner arc
}

void DrawTriStraightEdge(double startX, double startY, double endX, double endY, int thickness, uint borderColor) {
   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 = 1.5;                                //--- 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(triangleHighResCanvas, verticesX, verticesY, borderColor); //--- Fill quadrilateral for edge
}

void DrawTriCornerArcPrecise(int cornerIndex, int thickness, uint borderColor) {
   double centerX = triangleArcCentersX[cornerIndex],  centerY = triangleArcCentersY[cornerIndex]; //--- Get arc center
   double radius  = (double)triangleCornerRadiusPixels * 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(AngleInArcSweep(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 < triangleHighResWidth && pixelY >= 0 && pixelY < triangleHighResHeight) //--- Check if within bounds
               triangleHighResCanvas.PixelSet(pixelX, pixelY, borderColor); //--- Set pixel
         }
      }
   }
}

ここでは、FillRoundedTriangleHiRes関数を実装し、高解像度canvas上へ角丸三角形の内部塗りつぶしを描画します。この処理では、スキャンラインアルゴリズムを使用します。まず、鋭角の頂点情報から最小Y座標と最大Y座標を取得し、描画対象となる垂直範囲を決定します。次に、各整数Y座標についてループ処理をおこないます。この際、サブピクセル精度を向上させるため、半ピクセル分のオフセットを加えた位置を使用します。各スキャンラインでは、まず3つの接線エッジとの交点を計算します。交点計算では、対象範囲内に存在する場合に線形補間を使用してX座標を求めます。さらに、角丸部分の円弧については、円の方程式を利用して、そのY位置におけるΔXを計算します。取得した候補点については、AngleInArcSweepによって角度範囲内に存在するかを確認し、円弧部分に属する点だけを追加します。その後、交点リストをバブルソートによって並び替えます。並び替えられた交点はペアとして処理され、それぞれの間の範囲をPixelSetによって塗りつぶします。この方法により、事前計算されたジオメトリ情報を活用しながら、正確でアンチエイリアス対応の滑らかな曲線を持つ内部領域を描画できます。

次に、境界線を描画するためにDrawRoundedTriangleBorderHiRes関数を実装します。この関数では、まず境界線の太さをスーパーサンプリング倍率に合わせてスケーリングします。その後、各辺を処理し、DrawTriStraightEdgeによって直線部分を描画します。続いて、各角についてDrawTriCornerArcPreciseを呼び出し、円弧部分を追加することで、完全な太さを持つアウトラインを形成します。DrawTriStraightEdgeでは、接点から辺方向ベクトルと垂直ベクトルを計算します。その後、接続部分が滑らかになるように始点と終点を少し延長し、半分の境界線幅だけオフセットした四角形ストリップを定義します。この領域は、先に作成したFillQuadrilateral関数によって塗りつぶされます。これにより、すべての辺で均一な幅を持つ滑らかな境界線を生成できます。

最後に、DrawTriCornerArcPrecise関数では、各角の曲線境界を描画します。まず、内側半径と外側半径を計算し、その間に存在するリング状の領域を求めます。その後、少し広げたピクセル範囲を走査し、各ピクセルについて距離条件とAngleInArcSweepによる角度条件を確認します。条件を満たすピクセルのみへ境界線カラーを設定します。また、範囲外アクセスを防ぐための境界チェックもおこないます。これにより、高解像度レンダリング環境でも、ギザギザのない高品質な角丸境界線を生成できます。これらの関数が完成したことで、角丸三角形を生成するために必要な描画処理を組み合わせる準備が整いました。

void DrawRoundedTriangle() {
   uint backgroundColorARGB     = ColorToARGBWithOpacity(triangleBackgroundColor,     triangleBackgroundOpacityPercent); //--- Get background ARGB
   uint borderColorARGB = ColorToARGBWithOpacity(triangleBorderColor, triangleBorderOpacityPercent); //--- Get border ARGB

   FillRoundedTriangleHiRes(backgroundColorARGB);               //--- Fill high-res triangle

   if(triangleShowBorder && triangleBorderThicknessPixels > 0)  //--- Check if border should be shown
      DrawRoundedTriangleBorderHiRes(borderColorARGB);          //--- Draw border on high-res

   BicubicDownsample(triangleCanvas, triangleHighResCanvas);    //--- Downsample to display canvas

   triangleCanvas.FontSet("Arial", 13, FW_NORMAL);              //--- Set font for text
   string displayText = "Rounded Triangle";                     //--- Set display text
   int textWidth, textHeight;                                   //--- Declare text dimensions
   triangleCanvas.TextSize(displayText, textWidth, textHeight); //--- Get text size
   int textPositionX = 10 + (triangleBaseWidthPixels   - textWidth)  / 2; //--- Compute text X position
   int textPositionY = 10 + (computedTriangleHeightPixels - textHeight) / 2; //--- Compute text Y position
   triangleCanvas.TextOut(textPositionX, textPositionY, displayText, (uint)0xFF000000, TA_LEFT); //--- Draw text on canvas
}

DrawRoundedTriangle関数を定義し、canvas上で角丸三角形を描画する処理全体を管理します。まず、背景色と境界線色をColorToARGBWithOpacity関数によってARGB形式へ変換します。この処理では透明度も同時に適用されるため、図形に半透明効果を持たせることができ、視覚的な奥行きを表現できます。次に、内部領域を描画するため、事前計算されたジオメトリ情報を利用してFillRoundedTriangleHiResを呼び出します。この関数へ背景色のARGB値を渡すことで、高解像度canvas上へ角丸三角形の内部を塗りつぶします。境界線を有効にする設定であるtriangleShowBorderがtrueで、かつ境界線の太さが正の値である場合は、DrawRoundedTriangleBorderHiResを実行します。これにより、指定されたARGB形式の境界線カラーを使用してアウトラインを追加します。その後、高解像度canvasから通常解像度canvasへBicubicDownsampleを使用して縮小処理をおこないます。この処理によって高解像度で描画された情報が滑らかに統合され、アンチエイリアス処理された高品質な表示結果が得られます。

最後に、通常canvas上へ識別用のラベルを描画します。FontSetを使用してフォントをArial、サイズ13、太さFW_NORMALへ設定します。続いて、TextSizeによって「Rounded Triangle」の文字サイズを取得し、中央配置となる位置を計算します。その後、TextOutを使用して、左寄せ設定かつ単色の黒色 (0xFF000000)でテキストを描画します。これにより、表示されている図形の種類を簡単に識別できるようになります。なお、カラー形式については任意の設定が可能です。次に、角丸長方形の場合と同じ流れで初期化処理へこの描画ロジックを組み込み、チャート上へ三角形を表示します。これにより、初期化コード全体は以下のような構成になります。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   supersamplingFactor = supersamplingLevel;                    //--- Assign supersampling factor from input
   if(supersamplingFactor < 1) {                                //--- Check if supersampling factor is less than 1
      Print("Warning: supersamplingLevel must be at least 1. Setting to 1."); //--- Print warning message
      supersamplingFactor = 1;                                  //--- Set supersampling factor to minimum value
   }

   computedTriangleHeightPixels = (int)MathRound((double)triangleBaseWidthPixels * triangleHeightAsPercentOfWidth / 100.0); //--- Calculate triangle height based on width and percentage
   if(computedTriangleHeightPixels < 10) {                      //--- Check if computed height is too small
      Print("Warning: Computed triangle height too small (" + string(computedTriangleHeightPixels) + "px). Minimum set to 10."); //--- Print warning message
      computedTriangleHeightPixels = 10;                        //--- Set minimum height value
   }

   int rectangleCanvasWidth = rectangleWidthPixels  + 40;       //--- Compute rectangle canvas width with padding
   int rectangleCanvasHeight = rectangleHeightPixels + 40;      //--- Compute rectangle canvas height with padding
   int rectanglePositionY       = shapesPositionY;              //--- Set rectangle Y position

   if(!rectangleCanvas.CreateBitmapLabel(0, 0, rectangleCanvasName, shapesPositionX, rectanglePositionY,
                                    rectangleCanvasWidth, rectangleCanvasHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create rectangle canvas bitmap label
      Print("Error creating rectangle canvas: ", GetLastError()); //--- Print error message if creation fails
      return(INIT_FAILED);                                        //--- Return initialization failure
   }

   if(!rectangleHighResCanvas.Create(rectangleCanvasName + "_hires",
                        rectangleCanvasWidth * supersamplingFactor,
                        rectangleCanvasHeight * supersamplingFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create high-res rectangle canvas
      Print("Error creating rectangle hi-res canvas: ", GetLastError()); //--- Print error message if creation fails
      return(INIT_FAILED);                                        //--- Return initialization failure
   }

   int triangleCanvasWidth  = triangleBaseWidthPixels   + 40;     //--- Compute triangle canvas width with padding
   int triangleCanvasHeight  = computedTriangleHeightPixels + 40; //--- Compute triangle canvas height with padding
   int trianglePositionY        = rectanglePositionY + rectangleCanvasHeight + shapesGapPixels; //--- Set triangle Y position below rectangle

   triangleHighResWidth = triangleCanvasWidth  * supersamplingFactor;    //--- Compute high-res triangle width
   triangleHighResHeight  = triangleCanvasHeight  * supersamplingFactor; //--- Compute high-res triangle height

   if(!triangleCanvas.CreateBitmapLabel(0, 0, triangleCanvasName, shapesPositionX, trianglePositionY,
                                   triangleCanvasWidth, triangleCanvasHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create triangle canvas bitmap label
      Print("Error creating triangle canvas: ", GetLastError());  //--- Print error message if creation fails
      return(INIT_FAILED);                                        //--- Return initialization failure
   }

   if(!triangleHighResCanvas.Create(triangleCanvasName + "_hires",
                       triangleHighResWidth, triangleHighResHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create high-res triangle canvas
      Print("Error creating triangle hi-res canvas: ", GetLastError()); //--- Print error message if creation fails
      return(INIT_FAILED);                                      //--- Return initialization failure
   }

   rectangleCanvas.Erase(ColorToARGB(clrNONE, 0));              //--- Clear rectangle canvas
   rectangleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));       //--- Clear high-res rectangle canvas
   triangleCanvas.Erase(ColorToARGB(clrNONE, 0));               //--- Clear triangle canvas
   triangleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));        //--- Clear high-res triangle canvas

   PrecomputeTriangleGeometry();                                //--- Precompute triangle geometry
   DrawRoundedRectangle();                                      //--- Draw rounded rectangle
   DrawRoundedTriangle();                                       //--- Draw rounded triangle

   rectangleCanvas.Update();                                    //--- Update rectangle canvas display
   triangleCanvas.Update();                                     //--- Update triangle 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

   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
   }
}

OnDeinitイベントハンドラでは、プログラムが削除される際のリソース解放処理をおこないます。まず、長方形と三角形それぞれについて、高解像度canvasと通常解像度canvasをDestroyメソッドによって破棄します。その後、ObjectDeleteを使用して関連するチャートオブジェクトを削除し、メモリを解放すると同時に、画面上に残った描画要素を取り除きます。最後に、ChartRedrawを呼び出してチャートを再描画し、不要な描画跡や残留表示が残らない状態にします。

次に、 OnChartEventハンドラでは、チャートサイズ変更やプロパティ変更によって発生するCHARTEVENT_CHART_CHANGEイベントを処理します。このイベントが発生した場合、まず長方形用の両方のcanvasをEraseによってクリアします。この際、clrNONEから生成した透明なARGBカラーを使用します。その後、DrawRoundedRectangleを呼び出して角丸長方形を再描画し、Updateメソッドによって表示を更新します。同様に、三角形用canvasについてもクリアし、DrawRoundedTriangleによって再描画した後、表示を更新します。この処理により、チャートサイズや設定が変更された場合でも、図形表示が正しく維持され、柔軟に対応できるようになります。コンパイルすると、次の結果が得られます。

角丸三角形と角丸長方形のレンダリング

表示結果から、角丸三角形と角丸長方形の描画が正常に完成し、当初の目的を達成できたことを確認できます。残る作業はシステムの動作確認であり、これは次のセクションで扱います。


バックテスト

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

バックテストGIF


結論

本記事では、MQL5のcanvasを使用し、角丸長方形と角丸三角形を描画するためのベクトルベース手法について解説しました。スーパーサンプリングによるアンチエイリアス処理を導入し、スキャンラインによる塗りつぶし、円弧や接線のためのジオメトリ事前計算、さらに境界線描画処理を実装することで、滑らかでカスタマイズ可能な図形生成を実現しました。このアプローチは、今後の取引ツールにおけるモダンなUI要素を構築するための基盤となります。サイズ、角の半径、境界線、透明度などを入力パラメータとして制御できるため、柔軟なインターフェース設計が可能になります。次回は、これら2つの図形を組み合わせ、さまざまなユーティリティで利用できる、ポインター付きのモダンな吹き出しUIを作成する方法について解説します。どうぞお楽しみに。

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

最後のコメント | ディスカッションに移動 (1)
Nauris Zukas
Nauris Zukas | 13 5月 2026 において 07:52
この記事、ありがとうございます!とても参考になり、詳しく説明されています。
角丸長方形にベベルエッジ効果(3Dライティング――上・左が白、下・右が黒)を追加しようとしています。
直線のエッジにはベベル効果をうまく実装できたのですが、角でそれらをつなぐのに苦労しています。
下の画像で、線が交差している部分に赤い丸で印をつけた箇所をご覧ください。
何かアドバイスはありますか?


//+------------------------------------------------------------------+
//| 面取りエフェクト
//+------------------------------------------------------------------+
void DrawBeveledEdges(int posX, int posY, int width, int height, int radius, int scaleFactor)
  {
   color lightColor = clrWhite;
   uint lightARGB = ColorToARGBWithOpacity(lightColor, 20);

   color darkColor = clrBlack;
   uint darkARGB = ColorToARGBWithOpacity(darkColor, 35);

   int bevelWidth = 3 * scaleFactor;

// ===== 明るい面(左上) =====

// 上部の水平部分
   for(int y = 0; y < bevelWidth; y++)
     {
      for(int x = radius; x < width - radius; x++)
        {
         rectangleHighResCanvas.PixelSet(posX + x, posY + y, lightARGB);
        }
     }

// 左側の縦部分
   for(int x = 0; x < bevelWidth; x++)
     {
      for(int y = radius; y < height - radius; y++)
        {
         rectangleHighResCanvas.PixelSet(posX + x, posY + y, lightARGB);
        }
     }

// 左上のARC - 明るい色のみ
   for(int dy = -radius; dy <= 0; dy++)
     {
      for(int dx = -radius; dx <= 0; dx++)
        {
         double dist = MathSqrt((double)(dx*dx + dy*dy));

         if(dist >= radius - bevelWidth && dist <= radius + 1)
           {
            int pixX = posX + radius + dx;
            int pixY = posY + radius + dy;

            if(pixX >= posX && pixY >= posY)
               rectangleHighResCanvas.PixelSet(pixX, pixY, lightARGB);
           }
        }
     }

// 右上のARC - 明るい色のみ(右上隅)
   for(int dy = -radius; dy <= 0; dy++)
     {
      for(int dx = 0; dx <= radius; dx++)
        {
         double dist = MathSqrt((double)(dx*dx + dy*dy));

         if(dist >= radius - bevelWidth && dist <= radius + 1)
           {
            int pixX = posX + width - radius + dx;
            int pixY = posY + radius + dy;

            if(pixX < posX + width && pixY >= posY)
               rectangleHighResCanvas.PixelSet(pixX, pixY, lightARGB);
           }
        }
     }

// ===== ダークエッジ(右下) =====

// 下部の水平部分
   for(int y = height - bevelWidth; y < height; y++)
     {
      for(int x = radius; x < width - radius; x++)
        {
         rectangleHighResCanvas.PixelSet(posX + x, posY + y, darkARGB);
        }
     }

// 右側の縦部分
   for(int x = width - bevelWidth; x < width; x++)
     {
      for(int y = radius; y < height - radius; y++)
        {
         rectangleHighResCanvas.PixelSet(posX + x, posY + y, darkARGB);
        }
     }

// 右下のARC - 濃い色のみ
   for(int dy = 0; dy <= radius; dy++)
     {
      for(int dx = 0; dx <= radius; dx++)
        {
         double dist = MathSqrt((double)(dx*dx + dy*dy));

         if(dist >= radius - bevelWidth && dist <= radius + 1)
           {
            int pixX = posX + width - radius + dx;
            int pixY = posY + height - radius + dy;

            if(pixX < posX + width && pixY < posY + height)
               rectangleHighResCanvas.PixelSet(pixX, pixY, darkARGB);
           }
        }
     }

// 左下のARC - 暗い色のみ(左下隅)
   for(int dy = 0; dy <= radius; dy++)
     {
      for(int dx = -radius; dx <= 0; dx++)
        {
         double dist = MathSqrt((double)(dx*dx + dy*dy));

         if(dist >= radius - bevelWidth && dist <= radius + 1)
           {
            int pixX = posX + radius + dx;
            int pixY = posY + height - radius + dy;

            if(pixX >= posX && pixY < posY + height)
               rectangleHighResCanvas.PixelSet(pixX, pixY, darkARGB);
           }
        }
     }
  }
//+------------------------------------------------------------------+


追伸 -今後の記事の アイデア:
ドロップシャドウとグロー効果 - UI要素に奥行きを加える方法
グラデーション塗り - 色の変化で3Dのような外観を作る
アンチエイリアシングの手法 - プロフェッショナルな仕上がりのためのエッジの滑らか化
インナーシャドウ - 3Dベベル効果の強化
テクスチャとノイズパターン - 平面的な形状にリアリティを加える
EAのサンプル EAのサンプル
一般的なMACDを使ったEAを例として、MQL4開発の原則を紹介します。
プライスアクション分析ツールキットの開発(第61回):3スイング検証による構造的傾斜トレンドラインブレイクアウト プライスアクション分析ツールキットの開発(第61回):3スイング検証による構造的傾斜トレンドラインブレイクアウト
3点のスイングによる検証を基準として、客観的なプライスアクションシグナルを生成する、傾斜トレンドラインブレイクアウトツールを紹介します。このシステムは、スイング検出、トレンドライン構築、そしてクロス判定ロジックを用いたブレイクアウト確認を自動化することで、ノイズを低減し、運用・執行判断を標準化します。戦略ルールを解説し、MQL5による実装を示し、テスト結果を検証します。このツールは自動売買を目的としたものではなく、分析およびシグナル確認を支援するためのものです。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
ラリー・ウィリアムズの『市場の秘密』(第11回):カスタムインジケータによるスマッシュデー反転の検出 ラリー・ウィリアムズの『市場の秘密』(第11回):カスタムインジケータによるスマッシュデー反転の検出
ラリー・ウィリアムズのスマッシュデー反転ルールを、確認済みセットアップを矢印で表示する実用的なMQL5インジケータへ変換します。バッファのバインド、プロットプロパティの設定、履歴データへのマッピング、そしてOnCalculate内でのリアルタイム更新までを、段階的に解説します。調整可能なルックバックパラメータと見やすいチャート描画により、有効な反転をすばやく検出できる一方で、最終的な売買判断は裁量と市場状況に委ねられます。