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

結論
本記事では、MQL5のcanvasを使用し、角丸長方形と角丸三角形を描画するためのベクトルベース手法について解説しました。スーパーサンプリングによるアンチエイリアス処理を導入し、スキャンラインによる塗りつぶし、円弧や接線のためのジオメトリ事前計算、さらに境界線描画処理を実装することで、滑らかでカスタマイズ可能な図形生成を実現しました。このアプローチは、今後の取引ツールにおけるモダンなUI要素を構築するための基盤となります。サイズ、角の半径、境界線、透明度などを入力パラメータとして制御できるため、柔軟なインターフェース設計が可能になります。次回は、これら2つの図形を組み合わせ、さまざまなユーティリティで利用できる、ポインター付きのモダンな吹き出しUIを作成する方法について解説します。どうぞお楽しみに。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/21264
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
プライスアクション分析ツールキットの開発(第61回):3スイング検証による構造的傾斜トレンドラインブレイクアウト
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
ラリー・ウィリアムズの『市場の秘密』(第11回):カスタムインジケータによるスマッシュデー反転の検出
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
角丸長方形にベベルエッジ効果(3Dライティング――上・左が白、下・右が黒)を追加しようとしています。
直線のエッジにはベベル効果をうまく実装できたのですが、角でそれらをつなぐのに苦労しています。
下の画像で、線が交差している部分に赤い丸で印をつけた箇所をご覧ください。
何かアドバイスはありますか?
追伸 -今後の記事の アイデア:
ドロップシャドウとグロー効果 - UI要素に奥行きを加える方法
グラデーション塗り - 色の変化で3Dのような外観を作る
アンチエイリアシングの手法 - プロフェッショナルな仕上がりのためのエッジの滑らか化
インナーシャドウ - 3Dベベル効果の強化
テクスチャとノイズパターン - 平面的な形状にリアリティを加える