MQL5取引ツール(第18回):向き制御対応の角丸吹き出し
はじめに
第17回では、Canvasを使用してMetaQuotes Language 5 (MQL5)上で角丸長方形と三角形をベクター方式で描画する方法について解説し、スーパーサンプリングによるアンチエイリアス処理も紹介しました。第18回となる本記事では、これら2つの図形を組み合わせ、ポインタの向きを上、下、左、右に切り替えられる角丸の吹き出しを作成します。この実装では、ポインタの位置、枠線、および不透明度を調整できるため、トレードインターフェース向けの柔軟なUI部品として活用できます。本記事では以下のトピックを扱います。
本記事を読み終える頃には、高度なアプリケーション向けに自由にカスタマイズできる、実用的な吹き出しシステムを構築できるようになります。それでは始めましょう。
向きを制御できる角丸吹き出しの仕組み
向きを制御できる角丸吹き出しは、角丸長方形の本体と三角形のポインタを組み合わせた図形です。列挙型によってポインタの向きを上、下、左、右へ動的に切り替えることができるため、トレードインターフェースにおけるアラートやツールチップなど、多用途なUI部品として利用できます。ポインタの向きによってレイアウトが決まり、本体はポインタに対して適切な位置へ移動します。また、位置合わせのためのベースオフセットが適用され、両方の図形が滑らかに結合されることで、接続部分に視覚的な不連続が生じないようになっています。このベクター方式では、スーパーサンプリングによるアンチエイリアス処理を利用することで、拡大・縮小しても高品質な描画を維持でき、MQL5アプリケーションにおける視認性とデザイン性を向上させることができます。本記事では、まずポインタの向きに応じて吹き出し本体とポインタのジオメトリを事前計算し、その後、水平または垂直方向に対応したスキャンラインアルゴリズムを用いて図形全体を塗りつぶします。さらに、枠線は分割して描画し、各辺を適切に延長することで、接続部分を継ぎ目なく滑らかに仕上げます。以下は、本記事で目指す吹き出しの完成イメージです。

MQL5での実装
MQL5で本プログラムを拡張するために、まず新しい吹き出し機能とポインタの向きを制御できるよう、新たな#define、グローバル変数、および入力パラメータを追加します。
//+------------------------------------------------------------------+ //| Rounded Rectangle & Triangle PART2.mq5 | //| Copyright 2026, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #property strict //+------------------------------------------------------------------+ //| Enumerations | //+------------------------------------------------------------------+ enum BUBBLE_ORIENTATION { ORIENT_UP = 0, // Pointer faces up ORIENT_DOWN = 1, // Pointer faces down ORIENT_LEFT = 2, // Pointer faces left ORIENT_RIGHT = 3 // Pointer faces right }; input group "Bubble Shape" input int bubbleBodyWidthPixels = 250; // Bubble body width input int bubbleBodyHeightPixels = 100; // Bubble body height input int bubbleBodyCornerRadiusPixels = 5; // Bubble body corner radius input int bubblePointerBaseWidthPixels = 60; // Bubble pointer base width input int bubblePointerHeightPixels = 40; // Bubble pointer height input int bubblePointerApexRadiusPixels = 12; // Bubble pointer apex radius input int bubblePointerBaseOffsetPixels = 0; // Bubble pointer offset from center (0=centered, +/-=shift) input BUBBLE_ORIENTATION bubblePointerOrientation = ORIENT_UP; // Bubble pointer orientation input bool bubbleShowBorder = true; // Show bubble border input int bubbleBorderThicknessPixels = 2; // Bubble border thickness input color bubbleBorderColor = clrGreen; // Bubble border color input int bubbleBorderOpacityPercent = 80; // Bubble border opacity (0-100%) input color bubbleBackgroundColor = clrGreen; // Bubble background color input int bubbleBackgroundOpacityPercent = 30; // Bubble background opacity (0-100%) input group "General" input double borderExtensionMultiplier = 0.23; // Border extension multiplier (fraction of thickness) //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ CCanvas bubbleCanvas, bubbleHighResCanvas; //--- Declare bubble canvas objects string bubbleCanvasName = "BubbleCanvas"; //--- Set bubble canvas name double bubbleBodyLeft, bubbleBodyTop, bubbleBodyRight, bubbleBodyBottom; //--- Store bubble body coordinates double bubblePointerVerticesX[3], bubblePointerVerticesY[3]; //--- Store bubble pointer vertices X and Y double bubblePointerArcCentersX[3], bubblePointerArcCentersY[3]; //--- Store bubble pointer arc centers X and Y double bubblePointerTangentPointsX[3][2], bubblePointerTangentPointsY[3][2]; //--- Store bubble pointer tangent points X and Y double bubblePointerArcStartAngles[3], bubblePointerArcEndAngles[3]; //--- Store bubble pointer arc start and end angles int bubblePointerApexIndex; //--- Store bubble pointer apex index double bubblePointerBaseStart, bubblePointerBaseEnd; //--- Store bubble pointer base start and end bool bubbleIsHorizontalOrientation; //--- Store if bubble orientation is horizontal
続いて、ポインタの向きを表すBUBBLE_ORIENTATION列挙型を定義します。この列挙型には、ORIENT_UP(0)、ORIENT_DOWN(1)、ORIENT_LEFT(2)、ORIENT_RIGHT(3)の各オプションを用意し、吹き出しのレイアウトを柔軟に制御できるようにします。次に、「Bubble Shape」入力グループでは、bubbleBodyWidthPixelsやbubbleBodyHeightPixelsといった吹き出し本体の寸法を指定するパラメータを追加します。そのほか、角丸半径を指定するbubbleBodyCornerRadiusPixels、ポインタの仕様を設定するbubblePointerBaseWidthPixels、bubblePointerHeightPixels、bubblePointerApexRadiusPixels、さらにポインタの中央位置を調整するオフセットbubblePointerBaseOffsetPixelsも追加します。また、列挙型で指定するポインタの向きに加え、枠線の表示/非表示、枠線の太さ、色、および不透明度など、これまでの図形と同様の設定項目も用意します。
枠線をより細かく調整するため、borderExtensionMultiplierという共通の入力パラメータも追加します。この値は枠線の太さに対する比率として使用され、辺を適度に延長することで、接合部を継ぎ目なく滑らかに描画できるようにします。グローバル変数にも、吹き出し専用のCanvasオブジェクトであるbubbleCanvasとbubbleHighResCanvas(「BubbleCanvas」)を追加します。また、吹き出し本体の境界はbubbleBodyLeftやbubbleBodyTopなどのdouble型変数に保持します。さらに、ポインタの頂点座標、円弧の中心座標、接線(3×2配列)、角度、ポインタ先端のインデックスbubblePointerApexIndex、ポインタ基部の開始点・終了点、そしてスキャンラインの方向を動的に切り替えるための真偽値bubbleIsHorizontalOrientationも保持します。次に、前回までの図形と同様の考え方に基づき、吹き出しを生成するための関数を定義していきます。まずは、吹き出しのジオメトリを事前計算する処理から実装します。
//+------------------------------------------------------------------+ //| Bubble Shape | //+------------------------------------------------------------------+ void PrecomputeBubbleGeometry() { int scalingFactor = supersamplingFactor; //--- Set scaling factor double baseOffset = 10.0 * scalingFactor; //--- Set base offset scaled double centeringAdjustment; //--- Declare centering adjustment if(bubblePointerOrientation == ORIENT_UP) { //--- Check for up orientation bubbleBodyLeft = baseOffset; //--- Set body left bubbleBodyTop = baseOffset + bubblePointerHeightPixels * scalingFactor; //--- Set body top bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom centeringAdjustment = (bubbleBodyWidthPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset double pointerCenterX = bubbleBodyLeft + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center X bubblePointerVerticesX[0] = pointerCenterX; //--- Set apex X bubblePointerVerticesY[0] = baseOffset; //--- Set apex Y bubblePointerVerticesX[1] = bubbleBodyLeft + actualOffset; //--- Set left X bubblePointerVerticesY[1] = bubbleBodyTop; //--- Set left Y bubblePointerVerticesX[2] = bubblePointerVerticesX[1] + bubblePointerBaseWidthPixels * scalingFactor; //--- Set right X bubblePointerVerticesY[2] = bubbleBodyTop; //--- Set right Y bubblePointerApexIndex = 0; //--- Set apex index bubblePointerBaseStart = bubblePointerVerticesX[1]; //--- Set base start bubblePointerBaseEnd = bubblePointerVerticesX[2]; //--- Set base end } else if(bubblePointerOrientation == ORIENT_DOWN) { //--- Check for down orientation bubbleBodyLeft = baseOffset; //--- Set body left bubbleBodyTop = baseOffset; //--- Set body top bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom centeringAdjustment = (bubbleBodyWidthPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset double pointerCenterX = bubbleBodyLeft + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center X bubblePointerVerticesX[0] = pointerCenterX; //--- Set apex X bubblePointerVerticesY[0] = bubbleBodyBottom + bubblePointerHeightPixels * scalingFactor; //--- Set apex Y bubblePointerVerticesX[1] = bubbleBodyLeft + actualOffset + bubblePointerBaseWidthPixels * scalingFactor; //--- Set right X bubblePointerVerticesY[1] = bubbleBodyBottom; //--- Set right Y bubblePointerVerticesX[2] = bubbleBodyLeft + actualOffset; //--- Set left X bubblePointerVerticesY[2] = bubbleBodyBottom; //--- Set left Y bubblePointerApexIndex = 0; //--- Set apex index bubblePointerBaseStart = bubblePointerVerticesX[2]; //--- Set base start bubblePointerBaseEnd = bubblePointerVerticesX[1]; //--- Set base end } else if(bubblePointerOrientation == ORIENT_LEFT) { //--- Check for left orientation bubbleBodyLeft = baseOffset + bubblePointerHeightPixels * scalingFactor; //--- Set body left bubbleBodyTop = baseOffset; //--- Set body top bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom centeringAdjustment = (bubbleBodyHeightPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset double pointerCenterY = bubbleBodyTop + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center Y bubblePointerVerticesX[0] = baseOffset; //--- Set apex X bubblePointerVerticesY[0] = pointerCenterY; //--- Set apex Y bubblePointerVerticesX[1] = bubbleBodyLeft; //--- Set bottom X bubblePointerVerticesY[1] = bubbleBodyTop + actualOffset + bubblePointerBaseWidthPixels * scalingFactor; //--- Set bottom Y bubblePointerVerticesX[2] = bubbleBodyLeft; //--- Set top X bubblePointerVerticesY[2] = bubbleBodyTop + actualOffset; //--- Set top Y bubblePointerApexIndex = 0; //--- Set apex index bubblePointerBaseStart = bubblePointerVerticesY[2]; //--- Set base start bubblePointerBaseEnd = bubblePointerVerticesY[1]; //--- Set base end } else { //--- Handle right orientation bubbleBodyLeft = baseOffset; //--- Set body left bubbleBodyTop = baseOffset; //--- Set body top bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom centeringAdjustment = (bubbleBodyHeightPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset double pointerCenterY = bubbleBodyTop + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center Y bubblePointerVerticesX[0] = bubbleBodyRight + bubblePointerHeightPixels * scalingFactor; //--- Set apex X bubblePointerVerticesY[0] = pointerCenterY; //--- Set apex Y bubblePointerVerticesX[1] = bubbleBodyRight; //--- Set top X bubblePointerVerticesY[1] = bubbleBodyTop + actualOffset; //--- Set top Y bubblePointerVerticesX[2] = bubbleBodyRight; //--- Set bottom X bubblePointerVerticesY[2] = bubbleBodyTop + actualOffset + bubblePointerBaseWidthPixels * scalingFactor; //--- Set bottom Y bubblePointerApexIndex = 0; //--- Set apex index bubblePointerBaseStart = bubblePointerVerticesY[1]; //--- Set base start bubblePointerBaseEnd = bubblePointerVerticesY[2]; //--- Set base end } ComputeBubbleTriangleRoundedCorners(); //--- Compute rounded corners for bubble pointer } void ComputeBubbleTriangleRoundedCorners() { double scaledRadius = (double)bubblePointerApexRadiusPixels * supersamplingFactor; //--- Scale apex radius int cornerIndex = bubblePointerApexIndex; //--- Set corner index to apex int previousIndex = (cornerIndex + 2) % 3; //--- Get previous index int nextIndex = (cornerIndex + 1) % 3; //--- Get next index double edgeA_X = bubblePointerVerticesX[cornerIndex] - bubblePointerVerticesX[previousIndex]; //--- Compute edge A X double edgeA_Y = bubblePointerVerticesY[cornerIndex] - bubblePointerVerticesY[previousIndex]; //--- Compute edge A Y double edgeA_Length = MathSqrt(edgeA_X*edgeA_X + edgeA_Y*edgeA_Y); //--- Compute edge A length edgeA_X /= edgeA_Length; edgeA_Y /= edgeA_Length; //--- Normalize edge A double edgeB_X = bubblePointerVerticesX[nextIndex] - bubblePointerVerticesX[cornerIndex]; //--- Compute edge B X double edgeB_Y = bubblePointerVerticesY[nextIndex] - bubblePointerVerticesY[cornerIndex]; //--- Compute edge B Y double edgeB_Length = MathSqrt(edgeB_X*edgeB_X + edgeB_Y*edgeB_Y); //--- Compute edge B length edgeB_X /= edgeB_Length; edgeB_Y /= edgeB_Length; //--- Normalize edge B double normalA_X = edgeA_Y, normalA_Y = -edgeA_X; //--- Compute normal A double normalB_X = edgeB_Y, normalB_Y = -edgeB_X; //--- Compute normal B double bisectorX = normalA_X + normalB_X, bisectorY = normalA_Y + normalB_Y; //--- Compute bisector double bisectorLength = MathSqrt(bisectorX*bisectorX + bisectorY*bisectorY); //--- Compute bisector length if(bisectorLength < 1e-12) { bisectorX = normalA_X; bisectorY = normalA_Y; bisectorLength = MathSqrt(bisectorX*bisectorX + bisectorY*bisectorY); } //--- Handle small bisector bisectorX /= bisectorLength; bisectorY /= bisectorLength; //--- Normalize bisector double cosInteriorAngle = (-edgeA_X)*edgeB_X + (-edgeA_Y)*edgeB_Y; //--- Compute cosine of interior angle if(cosInteriorAngle > 1.0) cosInteriorAngle = 1.0; //--- Clamp cosine upper if(cosInteriorAngle < -1.0) cosInteriorAngle = -1.0; //--- Clamp cosine lower double halfAngle = MathArccos(cosInteriorAngle) / 2.0; //--- Compute half angle double sinHalfAngle = MathSin(halfAngle); //--- Compute sine of half angle if(sinHalfAngle < 1e-12) sinHalfAngle = 1e-12; //--- Set minimum sine value double distanceToCenter = scaledRadius / sinHalfAngle; //--- Compute distance to arc center bubblePointerArcCentersX[cornerIndex] = bubblePointerVerticesX[cornerIndex] + bisectorX * distanceToCenter; //--- Set arc center X bubblePointerArcCentersY[cornerIndex] = bubblePointerVerticesY[cornerIndex] + bisectorY * distanceToCenter; //--- Set arc center Y double deltaX_A = bubblePointerVerticesX[cornerIndex] - bubblePointerVerticesX[previousIndex]; //--- Compute delta A X double deltaY_A = bubblePointerVerticesY[cornerIndex] - bubblePointerVerticesY[previousIndex]; //--- Compute delta A Y double lengthSquared_A = deltaX_A*deltaX_A + deltaY_A*deltaY_A; //--- Compute length squared A double interpolationFactor_A = ((bubblePointerArcCentersX[cornerIndex] - bubblePointerVerticesX[previousIndex])*deltaX_A + (bubblePointerArcCentersY[cornerIndex] - bubblePointerVerticesY[previousIndex])*deltaY_A) / lengthSquared_A; //--- Compute factor A bubblePointerTangentPointsX[cornerIndex][1] = bubblePointerVerticesX[previousIndex] + interpolationFactor_A * deltaX_A; //--- Set tangent point X arriving bubblePointerTangentPointsY[cornerIndex][1] = bubblePointerVerticesY[previousIndex] + interpolationFactor_A * deltaY_A; //--- Set tangent point Y arriving double deltaX_B = bubblePointerVerticesX[nextIndex] - bubblePointerVerticesX[cornerIndex]; //--- Compute delta B X double deltaY_B = bubblePointerVerticesY[nextIndex] - bubblePointerVerticesY[cornerIndex]; //--- Compute delta B Y double lengthSquared_B = deltaX_B*deltaX_B + deltaY_B*deltaY_B; //--- Compute length squared B double interpolationFactor_B = ((bubblePointerArcCentersX[cornerIndex] - bubblePointerVerticesX[cornerIndex])*deltaX_B + (bubblePointerArcCentersY[cornerIndex] - bubblePointerVerticesY[cornerIndex])*deltaY_B) / lengthSquared_B; //--- Compute factor B bubblePointerTangentPointsX[cornerIndex][0] = bubblePointerVerticesX[cornerIndex] + interpolationFactor_B * deltaX_B; //--- Set tangent point X leaving bubblePointerTangentPointsY[cornerIndex][0] = bubblePointerVerticesY[cornerIndex] + interpolationFactor_B * deltaY_B; //--- Set tangent point Y leaving bubblePointerArcStartAngles[cornerIndex] = MathArctan2(bubblePointerTangentPointsY[cornerIndex][1] - bubblePointerArcCentersY[cornerIndex], bubblePointerTangentPointsX[cornerIndex][1] - bubblePointerArcCentersX[cornerIndex]); //--- Set start angle bubblePointerArcEndAngles[cornerIndex] = MathArctan2(bubblePointerTangentPointsY[cornerIndex][0] - bubblePointerArcCentersY[cornerIndex], bubblePointerTangentPointsX[cornerIndex][0] - bubblePointerArcCentersX[cornerIndex]); //--- Set end angle for(int i = 0; i < 3; i++) { //--- Loop over corners if(i == bubblePointerApexIndex) continue; //--- Skip apex corner bubblePointerTangentPointsX[i][0] = bubblePointerVerticesX[i]; //--- Set tangent X leaving to vertex bubblePointerTangentPointsY[i][0] = bubblePointerVerticesY[i]; //--- Set tangent Y leaving to vertex bubblePointerTangentPointsX[i][1] = bubblePointerVerticesX[i]; //--- Set tangent X arriving to vertex bubblePointerTangentPointsY[i][1] = bubblePointerVerticesY[i]; //--- Set tangent Y arriving to vertex } }
まず、吹き出しのレイアウトを構築するために、PrecomputeBubbleGeometry関数を実装します。この関数では、最初にスーパーサンプリング係数をローカル変数へ割り当て、パディング用の基準オフセットをスケーリングします。その後、ポインタを配置するための中央位置調整値を定義します。bubblePointerOrientationの値に応じて、吹き出し本体の座標を設定します。 ORIENT_UPの場合、ポインタの下側に本体を配置します。スケーリングされた入力値を使用して、左、上、右、下の各座標を計算します。その後、適用されたオフセットを考慮して中央位置を調整し、ポインタの中心X座標を決定します。そして、ポインタ先端を上部中央に配置し、左右の頂点を本体上辺に配置する形で頂点を定義します。この場合、bubblePointerApexIndexを0に設定し、ポインタ基部の開始点と終了点を左から右方向のX座標として設定します。
ORIENT_DOWNの場合は、本体をポインタの上側に配置します。先端位置を下側へ反転させ、頂点の順序を一貫した描画方向になるよう調整します。また、ポインタ基部の開始点と終了点も適切に更新します。同様に、ORIENT_LEFTの場合は、本体をポインタの右側へ移動させます。ポインタ先端を左側に設定し、本体左辺に下側および上側の頂点を配置します。また、基部はY座標によって定義します。ORIENT_RIGHTの場合は、これを左右反転した形で処理し、必要な調整を行います。この条件分岐による設定により、方向に応じて柔軟にジオメトリを変更できるようになります。その結果、ポインタと本体を重なりなく滑らかに統合できる構造になります。
次に、ポインタ先端を丸めるため、ComputeBubbleTriangleRoundedCorners関数を呼び出します。この処理では、先端の角丸半径をスケーリングし、bubblePointerApexIndexによって指定されたポインタ先端の角に重点を置いて計算を行います。三角形の事前計算と同様に、辺のベクトルと法線を計算し、二等分線を形成します。その後、中心位置までの距離を求めるために半角の正弦値を計算し、投影処理によって正接を求めます。そして、MathArctan2関数を使用して開始角度と終了角度を設定します。最後に、先端以外の角(ポインタ基部)については、接線を直接頂点に設定します。これらの部分では角丸処理を無効化し、吹き出し本体と平滑に接続されるようにします。これは、接続部分に不要な曲線を発生させず、自然で美しい吹き出し形状を実現するために重要な処理です。円弧ベースのピクセル描画をおこなうためには、ヘルパー関数を用意する必要があります。
bool BubbleAngleInArcSweep(int cornerIndex, double angle) { double twoPi = 2.0 * M_PI; //--- Define two pi constant double startAngleMod = MathMod(bubblePointerArcStartAngles[cornerIndex] + twoPi, twoPi); //--- Modulo start angle double endAngleMod = MathMod(bubblePointerArcEndAngles[cornerIndex] + twoPi, twoPi); //--- Modulo end angle angle = MathMod(angle + twoPi, twoPi); //--- Modulo angle double ccwSpan = MathMod(endAngleMod - startAngleMod + twoPi, twoPi); //--- Compute CCW span if(ccwSpan <= M_PI) { //--- Check if short way is CCW double relativeAngle = MathMod(angle - startAngleMod + twoPi, twoPi); //--- Compute relative angle return(relativeAngle <= ccwSpan + 1e-6); //--- Return if within CCW span } else { //--- Else short way is CW double cwSpan = twoPi - ccwSpan; //--- Compute CW span double relativeAngle = MathMod(angle - endAngleMod + twoPi, twoPi); //--- Compute relative angle return(relativeAngle <= cwSpan + 1e-6); //--- Return if within CW span } }
BubbleAngleInArcSweep関数を実装し、吹き出しポインタの特定の角において、指定された角度が円弧の範囲内に含まれるかどうかを判定します。まず、2πを表す定数を定義し、MathModを使用して開始角度、終了角度、および入力された角度を正規化します。これにより、すべての角度が0から2πの範囲内になるよう調整され、角度のラップ処理や負の値にも対応できます。次に、正規化された開始角度と終了角度の間の反時計回り方向の範囲(CCWスパン)を計算します。その値がπ以下の場合(つまり短い方の円弧を示す場合)は、開始角度からの相対角度がこの範囲内にあるかを、小さなイプシロン値を用いて精度を考慮しながら確認します。一方、範囲がπより大きい場合は、時計回り方向の範囲を2π - CCWスパンとして計算し、終了角度からの相対角度を確認します。このロジックにより、時計回り・反時計回りの両方の円弧方向に対応でき、描画時に正確なピクセル判定を行うことが可能になります。これは、吹き出しポインタの曲線的な枠線を、向きに関係なく滑らかでアーティファクト(描画欠陥)のない状態で描画するために重要な処理です。次に、角丸長方形の吹き出しを描画するために、以下の方法を使用します。
void FillBubbleRoundedRectangle(double left, double top, double width, double height, int radius, uint fillColor) { bubbleHighResCanvas.FillRectangle((int)(left + radius), (int)top, (int)(left + width - radius), (int)(top + height), fillColor); //--- Fill central rectangle bubbleHighResCanvas.FillRectangle((int)left, (int)(top + radius), (int)(left + radius), (int)(top + height - radius), fillColor); //--- Fill left strip bubbleHighResCanvas.FillRectangle((int)(left + width - radius), (int)(top + radius), (int)(left + width), (int)(top + height - radius), fillColor); //--- Fill right strip FillBubbleCircleQuadrant((int)(left + radius), (int)(top + radius), radius, fillColor, 2); //--- Fill top-left quadrant FillBubbleCircleQuadrant((int)(left + width - radius), (int)(top + radius), radius, fillColor, 1); //--- Fill top-right quadrant FillBubbleCircleQuadrant((int)(left + radius), (int)(top + height - radius), radius, fillColor, 3); //--- Fill bottom-left quadrant FillBubbleCircleQuadrant((int)(left + width - radius), (int)(top + height - radius), radius, fillColor, 4); //--- Fill bottom-right quadrant } void FillBubbleCircleQuadrant(int centerX, int centerY, int radius, uint fillColor, int quadrant) { double radiusDouble = (double)radius; //--- Convert radius to double for(int deltaY = -radius - 1; deltaY <= radius + 1; deltaY++) { //--- Loop over delta Y for(int deltaX = -radius - 1; deltaX <= radius + 1; deltaX++) { //--- Loop over delta X bool inQuadrant = false; //--- Initialize quadrant flag if(quadrant == 1 && deltaX >= 0 && deltaY <= 0) inQuadrant = true; //--- Check top-right else if(quadrant == 2 && deltaX <= 0 && deltaY <= 0) inQuadrant = true; //--- Check top-left else if(quadrant == 3 && deltaX <= 0 && deltaY >= 0) inQuadrant = true; //--- Check bottom-left else if(quadrant == 4 && deltaX >= 0 && deltaY >= 0) inQuadrant = true; //--- Check bottom-right if(!inQuadrant) continue; //--- Skip if not in quadrant double distance = MathSqrt(deltaX * deltaX + deltaY * deltaY); //--- Compute distance if(distance <= radiusDouble) //--- Check if within radius bubbleHighResCanvas.PixelSet(centerX + deltaX, centerY + deltaY, fillColor); //--- Set pixel } } }
ここでは、FillBubbleRoundedRectangle関数を実装し、高解像度Canvas上に吹き出し本体となる角丸長方形を塗りつぶして描画します。まず、ピクセル単位で正確に描画できるよう座標を整数値へ変換します。その後、FillRectangleを使用して角部分を除いた中央領域を塗りつぶし、さらに左右の縦方向の帯状領域を追加で描画することで、側面を隙間なく滑らかにつなげます。角丸処理を完成させるために、各コーナーに対してFillBubbleCircleQuadrant関数を呼び出します。この関数では、各象限に応じた処理を適用し、必要な4分の1円のピクセルのみを塗りつぶします。
FillBubbleCircleQuadrant関数では、まず半径を精度向上のためdouble型へ変換します。その後、アンチエイリアス処理に対応するため、拡張されたデルタ範囲をループ処理します。デルタ値の符号に基づいて対象となる象限を判定します(たとえば、Xが正でYが負の場合は第1象限)。次に、MathSqrtを使用して距離を計算し、その距離が半径以内にあるピクセルに対してPixelSetを用いて描画します。これにより、長方形部分の帯状領域と自然につながる滑らかな曲線を実現できます。 このモジュール化された塗りつぶし処理は、異なる向きの吹き出しを一貫して処理するうえで重要です。なぜなら、角丸長方形の描画ロジックを再利用しながら、ポインタとの接続部分に塗り残しが発生しないよう柔軟に対応できるためです。長方形部分の枠線を描画する処理は、比較的簡単で直感的に実装できます。次に、ポインタ部分となる角丸三角形を塗りつぶすためのロジックを定義していきます。
void FillBubbleRoundedTriangle(uint fillColor) { if(bubbleIsHorizontalOrientation) { //--- Check for horizontal orientation double minX = bubblePointerVerticesX[0], maxX = bubblePointerVerticesX[0]; //--- Initialize min and max X for(int i = 1; i < 3; i++) { //--- Loop over vertices if(bubblePointerVerticesX[i] < minX) minX = bubblePointerVerticesX[i]; //--- Update min X if(bubblePointerVerticesX[i] > maxX) maxX = bubblePointerVerticesX[i]; //--- Update max X } int xStart = (int)MathCeil(minX); //--- Compute start X int xEnd = (int)MathFloor(maxX); //--- Compute end X for(int x = xStart; x <= xEnd; x++) { //--- Loop over scanlines double scanlineX = (double)x + 0.5; //--- Set scanline X position double yIntersections[12]; //--- Declare intersections array int intersectionCount = 0; //--- Initialize intersection count for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) { //--- Loop over edges int nextIndex = (edgeIndex + 1) % 3; //--- Get next index if(edgeIndex != bubblePointerApexIndex && nextIndex != bubblePointerApexIndex) continue; //--- Skip non-apex edges double startX, startY, endX, endY; //--- Declare edge coordinates if(edgeIndex == bubblePointerApexIndex) { //--- Check if from apex startX = bubblePointerTangentPointsX[bubblePointerApexIndex][0]; //--- Set start X from tangent startY = bubblePointerTangentPointsY[bubblePointerApexIndex][0]; //--- Set start Y from tangent endX = bubblePointerVerticesX[nextIndex]; //--- Set end X to next vertex endY = bubblePointerVerticesY[nextIndex]; //--- Set end Y to next vertex } else { //--- Handle to apex startX = bubblePointerVerticesX[edgeIndex]; //--- Set start X from vertex startY = bubblePointerVerticesY[edgeIndex]; //--- Set start Y from vertex endX = bubblePointerTangentPointsX[bubblePointerApexIndex][1]; //--- Set end X to tangent endY = bubblePointerTangentPointsY[bubblePointerApexIndex][1]; //--- Set end Y to tangent } double edgeMinX = (startX < endX) ? startX : endX; //--- Compute edge min X double edgeMaxX = (startX > endX) ? startX : endX; //--- Compute edge max X if(scanlineX < edgeMinX || scanlineX > edgeMaxX) continue; //--- Skip if outside edge X if(MathAbs(endX - startX) < 1e-12) continue; //--- Skip if vertical edge double interpolationFactor = (scanlineX - startX) / (endX - startX); //--- Compute factor if(interpolationFactor < 0.0 || interpolationFactor > 1.0) continue; //--- Skip if outside segment yIntersections[intersectionCount++] = startY + interpolationFactor * (endY - startY); //--- Add intersection Y } { //--- Intersect apex arc block int cornerIndex = bubblePointerApexIndex; //--- Set corner index double centerX = bubblePointerArcCentersX[cornerIndex]; //--- Get center X double centerY = bubblePointerArcCentersY[cornerIndex]; //--- Get center Y double radius = (double)bubblePointerApexRadiusPixels * supersamplingFactor; //--- Get scaled radius double deltaX = scanlineX - centerX; //--- Compute delta X if(MathAbs(deltaX) <= radius) { //--- Check if within radius double deltaY = MathSqrt(radius * radius - deltaX * deltaX); //--- Compute delta Y double candidates[2]; //--- Declare candidates array candidates[0] = centerY - deltaY; //--- Set top candidate candidates[1] = centerY + deltaY; //--- Set bottom candidate for(int candidateIndex = 0; candidateIndex < 2; candidateIndex++) { //--- Loop over candidates double angle = MathArctan2(candidates[candidateIndex] - centerY, scanlineX - centerX); //--- Compute angle if(BubbleAngleInArcSweep(cornerIndex, angle)) //--- Check if in arc sweep yIntersections[intersectionCount++] = candidates[candidateIndex]; //--- Add intersection } } } for(int a = 0; a < intersectionCount - 1; a++) //--- Sort intersections (bubble sort) for(int b = a + 1; b < intersectionCount; b++) //--- Inner loop for sorting if(yIntersections[a] > yIntersections[b]) { //--- Check if swap needed double temp = yIntersections[a]; //--- Temporary store yIntersections[a] = yIntersections[b]; //--- Swap values yIntersections[b] = temp; //--- Complete swap } for(int pairIndex = 0; pairIndex + 1 < intersectionCount; pairIndex += 2) { //--- Loop over pairs int yTop = (int)MathCeil(yIntersections[pairIndex]); //--- Compute top Y int yBottom = (int)MathFloor(yIntersections[pairIndex + 1]); //--- Compute bottom Y for(int y = yTop; y <= yBottom; y++) //--- Loop over vertical span bubbleHighResCanvas.PixelSet(x, y, fillColor); //--- Set pixel with fill color } } } else { //--- Handle vertical orientation double minY = bubblePointerVerticesY[0], maxY = bubblePointerVerticesY[0]; //--- Initialize min and max Y for(int i = 1; i < 3; i++) { //--- Loop over vertices if(bubblePointerVerticesY[i] < minY) minY = bubblePointerVerticesY[i]; //--- Update min Y if(bubblePointerVerticesY[i] > maxY) maxY = bubblePointerVerticesY[i]; //--- Update max Y } int yStart = (int)MathCeil(minY); //--- Compute start Y int yEnd = (int)MathFloor(maxY); //--- Compute end Y for(int y = yStart; y <= yEnd; y++) { //--- Loop over scanlines double scanlineY = (double)y + 0.5; //--- Set scanline Y position double xIntersections[12]; //--- Declare intersections array int intersectionCount = 0; //--- Initialize intersection count for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) { //--- Loop over edges int nextIndex = (edgeIndex + 1) % 3; //--- Get next index if(edgeIndex != bubblePointerApexIndex && nextIndex != bubblePointerApexIndex) continue; //--- Skip non-apex edges double startX, startY, endX, endY; //--- Declare edge coordinates if(edgeIndex == bubblePointerApexIndex) { //--- Check if from apex startX = bubblePointerTangentPointsX[bubblePointerApexIndex][0]; //--- Set start X from tangent startY = bubblePointerTangentPointsY[bubblePointerApexIndex][0]; //--- Set start Y from tangent endX = bubblePointerVerticesX[nextIndex]; //--- Set end X to next vertex endY = bubblePointerVerticesY[nextIndex]; //--- Set end Y to next vertex } else { //--- Handle to apex startX = bubblePointerVerticesX[edgeIndex]; //--- Set start X from vertex startY = bubblePointerVerticesY[edgeIndex]; //--- Set start Y from vertex endX = bubblePointerTangentPointsX[bubblePointerApexIndex][1]; //--- Set end X to tangent endY = bubblePointerTangentPointsY[bubblePointerApexIndex][1]; //--- Set end Y to tangent } double edgeMinY = (startY < endY) ? startY : endY; //--- Compute edge min Y double edgeMaxY = (startY > endY) ? startY : endY; //--- Compute edge max Y if(scanlineY < edgeMinY || scanlineY > edgeMaxY) continue; //--- Skip if outside edge Y if(MathAbs(endY - startY) < 1e-12) continue; //--- Skip if horizontal double interpolationFactor = (scanlineY - startY) / (endY - startY); //--- Compute factor if(interpolationFactor < 0.0 || interpolationFactor > 1.0) continue; //--- Skip if outside segment xIntersections[intersectionCount++] = startX + interpolationFactor * (endX - startX); //--- Add intersection X } int cornerIndex = bubblePointerApexIndex; //--- Set corner index double centerX = bubblePointerArcCentersX[cornerIndex]; //--- Get center X double centerY = bubblePointerArcCentersY[cornerIndex]; //--- Get center Y double radius = (double)bubblePointerApexRadiusPixels * supersamplingFactor; //--- Get scaled radius double deltaY = scanlineY - centerY; //--- Compute delta Y if(MathAbs(deltaY) <= radius) { //--- Check if within radius double deltaX = MathSqrt(radius*radius - deltaY*deltaY); //--- Compute delta X double candidates[2]; //--- Declare candidates array candidates[0] = centerX - deltaX; //--- Set left candidate candidates[1] = centerX + deltaX; //--- Set right candidate for(int candidateIndex = 0; candidateIndex < 2; candidateIndex++) { //--- Loop over candidates double angle = MathArctan2(scanlineY - centerY, candidates[candidateIndex] - centerX); //--- Compute angle if(BubbleAngleInArcSweep(cornerIndex, angle)) //--- Check if in arc sweep xIntersections[intersectionCount++] = candidates[candidateIndex]; //--- Add intersection } } for(int a = 0; a < intersectionCount - 1; a++) //--- Sort intersections (bubble sort) for(int b = a + 1; b < intersectionCount; b++) //--- Inner loop for sorting if(xIntersections[a] > xIntersections[b]) { //--- Check if swap needed double temp = xIntersections[a]; //--- Temporary store xIntersections[a] = xIntersections[b]; //--- Swap values xIntersections[b] = temp; //--- Complete swap } for(int pairIndex = 0; pairIndex + 1 < intersectionCount; pairIndex += 2) { //--- Loop over pairs int xLeft = (int)MathCeil(xIntersections[pairIndex]); //--- Compute left X int xRight = (int)MathFloor(xIntersections[pairIndex + 1]); //--- Compute right X for(int x = xLeft; x <= xRight; x++) //--- Loop over horizontal span bubbleHighResCanvas.PixelSet(x, y, fillColor); //--- Set pixel with fill color } } } }
FillBubbleRoundedTriangle関数を実装し、高解像度Canvas上で吹き出しの三角形ポインタ部分を塗りつぶします。この処理では、効率を最適化するため、吹き出しの向きに応じてスキャンライン方向を切り替えます。bubbleIsHorizontalOrientationがtrueの場合、つまりポインタが左右方向(左向き・右向き)の場合は、水平方向のスキャン(x方向のループ)を使用します。それ以外の場合は、垂直方向のスキャン(y方向のループ)を使用します。水平方向の向きの場合、まずポインタ頂点から最小X値と最大X値を取得します。その後、半ピクセル分のオフセットを加えた整数X座標についてループ処理を行います。各X位置において、補間処理によってY方向の交点を取得します。この際、先端に隣接する辺のみを対象とし、基部側の辺は除外します。また、円弧部分については、デルタXにおける円の方程式を解くことで候補となる交点を追加します。ただし、その点が半径内に存在し、かつBubbleAngleInArcSweepによって円弧範囲内であることが確認された場合のみ有効とします。取得した交点はバブルソートによって並び替え、その後、交点のペア間に存在する垂直方向の範囲をPixelSetを使用してfillColorで塗りつぶします。
垂直方向の向きの場合は、この処理を反転して適用します。まず最小Y値と最大Y値を取得し、scanlineYを用いてY方向へループします。その各位置において、辺および円弧部分からX方向の交点を取得します。この場合、円弧候補の計算にはdeltaYを使用します。取得した交点を並び替え、水平方向の範囲を塗りつぶすことで、隙間のない完全な描画を実現します。 これにより、吹き出し本体となる角丸長方形と三角形ポインタの塗りつぶし処理が完成しました。次に、吹き出し全体を囲む枠線を描画するためのロジックを定義します。これを実現するために、以下の方法を使用します。
void DrawBubbleBorder(uint borderColorARGB) { int scaledThickness = bubbleBorderThicknessPixels * supersamplingFactor; //--- Scale border thickness int scaledBodyRadius = bubbleBodyCornerRadiusPixels * supersamplingFactor; //--- Scale body radius if(bubblePointerOrientation == ORIENT_UP || bubblePointerOrientation == ORIENT_DOWN) { //--- Check for up or down orientation if(bubblePointerOrientation == ORIENT_DOWN) { //--- Check for down orientation DrawBubbleHorizontalEdge(bubbleBodyLeft + scaledBodyRadius, bubbleBodyTop, bubbleBodyRight - scaledBodyRadius, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw top edge } else { //--- Handle up orientation if(bubblePointerBaseStart > bubbleBodyLeft + scaledBodyRadius) //--- Check if base start exceeds left radius DrawBubbleHorizontalEdge(bubbleBodyLeft + scaledBodyRadius, bubbleBodyTop, bubblePointerBaseStart, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw left segment of top edge if(bubblePointerBaseEnd < bubbleBodyRight - scaledBodyRadius) //--- Check if base end below right radius DrawBubbleHorizontalEdge(bubblePointerBaseEnd, bubbleBodyTop, bubbleBodyRight - scaledBodyRadius, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw right segment of top edge } if(bubblePointerOrientation == ORIENT_UP) { //--- Check for up orientation DrawBubbleHorizontalEdge(bubbleBodyRight - scaledBodyRadius, bubbleBodyBottom, bubbleBodyLeft + scaledBodyRadius, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw bottom edge } else { //--- Handle down orientation if(bubblePointerBaseStart > bubbleBodyLeft + scaledBodyRadius) //--- Check if base start exceeds left radius DrawBubbleHorizontalEdge(bubblePointerBaseStart, bubbleBodyBottom, bubbleBodyLeft + scaledBodyRadius, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw left segment of bottom edge if(bubblePointerBaseEnd < bubbleBodyRight - scaledBodyRadius) //--- Check if base end below right radius DrawBubbleHorizontalEdge(bubbleBodyRight - scaledBodyRadius, bubbleBodyBottom, bubblePointerBaseEnd, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw right segment of bottom edge } DrawBubbleVerticalEdge(bubbleBodyLeft, bubbleBodyBottom - scaledBodyRadius, bubbleBodyLeft, bubbleBodyTop + scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw left edge DrawBubbleVerticalEdge(bubbleBodyRight, bubbleBodyTop + scaledBodyRadius, bubbleBodyRight, bubbleBodyBottom - scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw right edge } else { //--- Handle left or right orientation DrawBubbleHorizontalEdge(bubbleBodyLeft + scaledBodyRadius, bubbleBodyTop, bubbleBodyRight - scaledBodyRadius, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw top edge DrawBubbleHorizontalEdge(bubbleBodyRight - scaledBodyRadius, bubbleBodyBottom, bubbleBodyLeft + scaledBodyRadius, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw bottom edge if(bubblePointerOrientation == ORIENT_RIGHT) { //--- Check for right orientation DrawBubbleVerticalEdge(bubbleBodyLeft, bubbleBodyBottom - scaledBodyRadius, bubbleBodyLeft, bubbleBodyTop + scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw left edge } else { //--- Handle left orientation if(bubblePointerBaseStart > bubbleBodyTop + scaledBodyRadius) //--- Check if base start exceeds top radius DrawBubbleVerticalEdge(bubbleBodyLeft, bubblePointerBaseStart, bubbleBodyLeft, bubbleBodyTop + scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw top segment of left edge if(bubblePointerBaseEnd < bubbleBodyBottom - scaledBodyRadius) //--- Check if base end below bottom radius DrawBubbleVerticalEdge(bubbleBodyLeft, bubbleBodyBottom - scaledBodyRadius, bubbleBodyLeft, bubblePointerBaseEnd, scaledThickness, borderColorARGB); //--- Draw bottom segment of left edge } if(bubblePointerOrientation == ORIENT_LEFT) { //--- Check for left orientation DrawBubbleVerticalEdge(bubbleBodyRight, bubbleBodyTop + scaledBodyRadius, bubbleBodyRight, bubbleBodyBottom - scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw right edge } else { //--- Handle right orientation if(bubblePointerBaseStart > bubbleBodyTop + scaledBodyRadius) //--- Check if base start exceeds top radius DrawBubbleVerticalEdge(bubbleBodyRight, bubbleBodyTop + scaledBodyRadius, bubbleBodyRight, bubblePointerBaseStart, scaledThickness, borderColorARGB); //--- Draw top segment of right edge if(bubblePointerBaseEnd < bubbleBodyBottom - scaledBodyRadius) //--- Check if base end below bottom radius DrawBubbleVerticalEdge(bubbleBodyRight, bubblePointerBaseEnd, bubbleBodyRight, bubbleBodyBottom - scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw bottom segment of right edge } } DrawBubbleCornerArc((int)(bubbleBodyLeft + scaledBodyRadius), (int)(bubbleBodyTop + scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, M_PI, M_PI * 1.5); //--- Draw top-left arc DrawBubbleCornerArc((int)(bubbleBodyRight - scaledBodyRadius), (int)(bubbleBodyTop + scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, M_PI * 1.5, M_PI * 2.0); //--- Draw top-right arc DrawBubbleCornerArc((int)(bubbleBodyLeft + scaledBodyRadius), (int)(bubbleBodyBottom - scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, M_PI * 0.5, M_PI); //--- Draw bottom-left arc DrawBubbleCornerArc((int)(bubbleBodyRight - scaledBodyRadius), (int)(bubbleBodyBottom - scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, 0.0, M_PI * 0.5); //--- Draw bottom-right arc for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) { //--- Loop over pointer edges int nextIndex = (edgeIndex + 1) % 3; //--- Get next index if(edgeIndex != bubblePointerApexIndex && nextIndex != bubblePointerApexIndex) continue; //--- Skip non-apex edges double startX, startY, endX, endY; //--- Declare edge coordinates if(edgeIndex == bubblePointerApexIndex) { //--- Check if from apex startX = bubblePointerTangentPointsX[bubblePointerApexIndex][0]; //--- Set start X from tangent startY = bubblePointerTangentPointsY[bubblePointerApexIndex][0]; //--- Set start Y from tangent endX = bubblePointerVerticesX[nextIndex]; //--- Set end X to next vertex endY = bubblePointerVerticesY[nextIndex]; //--- Set end Y to next vertex } else { //--- Handle to apex startX = bubblePointerVerticesX[edgeIndex]; //--- Set start X from vertex startY = bubblePointerVerticesY[edgeIndex]; //--- Set start Y from vertex endX = bubblePointerTangentPointsX[bubblePointerApexIndex][1]; //--- Set end X to tangent endY = bubblePointerTangentPointsY[bubblePointerApexIndex][1]; //--- Set end Y to tangent } DrawBubbleStraightEdge(startX, startY, endX, endY, scaledThickness, borderColorARGB); //--- Draw straight edge } DrawBubbleTriangleCornerArc(bubblePointerApexIndex, scaledThickness, borderColorARGB); //--- Draw apex arc } void DrawBubbleHorizontalEdge(double startX, double startY, double endX, double endY, int thickness, uint edgeColor) { DrawBubbleStraightEdge(startX, startY, endX, endY, thickness, edgeColor); //--- Draw horizontal edge using straight edge } void DrawBubbleVerticalEdge(double startX, double startY, double endX, double endY, int thickness, uint edgeColor) { DrawBubbleStraightEdge(startX, startY, endX, endY, thickness, edgeColor); //--- Draw vertical edge using straight edge } void DrawBubbleStraightEdge(double startX, double startY, double endX, double endY, int thickness, uint edgeColor) { double deltaX = endX - startX; //--- Compute delta X double deltaY = endY - startY; //--- Compute delta Y double edgeLength = MathSqrt(deltaX*deltaX + deltaY*deltaY); //--- Compute edge length if(edgeLength < 1e-6) return; //--- Return if length too small double perpendicularX = -deltaY / edgeLength; //--- Compute perpendicular X double perpendicularY = deltaX / edgeLength; //--- Compute perpendicular Y double edgeDirectionX = deltaX / edgeLength; //--- Compute edge direction X double edgeDirectionY = deltaY / edgeLength; //--- Compute edge direction Y double halfThickness = (double)thickness / 2.0; //--- Compute half thickness double extensionLength = borderExtensionMultiplier * (double)thickness; //--- Set extension length double extendedStartX = startX - edgeDirectionX * extensionLength; //--- Extend start X double extendedStartY = startY - edgeDirectionY * extensionLength; //--- Extend start Y double extendedEndX = endX + edgeDirectionX * extensionLength; //--- Extend end X double extendedEndY = endY + edgeDirectionY * extensionLength; //--- Extend end Y double verticesX[4], verticesY[4]; //--- Declare vertices arrays verticesX[0] = extendedStartX - perpendicularX * halfThickness; verticesY[0] = extendedStartY - perpendicularY * halfThickness; //--- Set vertex 0 verticesX[1] = extendedStartX + perpendicularX * halfThickness; verticesY[1] = extendedStartY + perpendicularY * halfThickness; //--- Set vertex 1 verticesX[2] = extendedEndX + perpendicularX * halfThickness; verticesY[2] = extendedEndY + perpendicularY * halfThickness; //--- Set vertex 2 verticesX[3] = extendedEndX - perpendicularX * halfThickness; verticesY[3] = extendedEndY - perpendicularY * halfThickness; //--- Set vertex 3 FillQuadrilateral(bubbleHighResCanvas, verticesX, verticesY, edgeColor); //--- Fill quadrilateral for edge } void DrawBubbleCornerArc(int centerX, int centerY, int radius, int thickness, uint edgeColor, double startAngle, double endAngle) { int halfThickness = thickness / 2; //--- Compute half thickness double outerRadius = (double)radius + halfThickness; //--- Compute outer radius double innerRadius = (double)radius - halfThickness; //--- Compute inner radius if(innerRadius < 0) innerRadius = 0; //--- Set inner radius to zero if negative int pixelRange = (int)(outerRadius + 2); //--- Compute pixel range for(int deltaY = -pixelRange; deltaY <= pixelRange; deltaY++) { //--- Loop over delta Y for(int deltaX = -pixelRange; deltaX <= pixelRange; deltaX++) { //--- Loop over delta X double distance = MathSqrt(deltaX * deltaX + deltaY * deltaY); //--- Compute distance if(distance < innerRadius || distance > outerRadius) continue; //--- Skip if outside radii double angle = MathArctan2((double)deltaY, (double)deltaX); //--- Compute angle if(IsAngleBetween(angle, startAngle, endAngle)) //--- Check if angle within range bubbleHighResCanvas.PixelSet(centerX + deltaX, centerY + deltaY, edgeColor); //--- Set pixel } } } void DrawBubbleTriangleCornerArc(int cornerIndex, int thickness, uint edgeColor) { double centerX = bubblePointerArcCentersX[cornerIndex]; //--- Get center X double centerY = bubblePointerArcCentersY[cornerIndex]; //--- Get center Y double radius = (double)bubblePointerApexRadiusPixels * supersamplingFactor; //--- Get scaled radius int halfThickness = thickness / 2; //--- Compute half thickness double outerRadius = radius + halfThickness; //--- Compute outer radius double innerRadius = radius - halfThickness; //--- Compute inner radius if(innerRadius < 0) innerRadius = 0; //--- Set inner radius to zero if negative int pixelRange = (int)(outerRadius + 2); //--- Compute pixel range for(int deltaY = -pixelRange; deltaY <= pixelRange; deltaY++) { //--- Loop over delta Y for(int deltaX = -pixelRange; deltaX <= pixelRange; deltaX++) { //--- Loop over delta X double distance = MathSqrt((double)(deltaX*deltaX + deltaY*deltaY)); //--- Compute distance if(distance < innerRadius || distance > outerRadius) continue; //--- Skip if outside radii double angle = MathArctan2((double)deltaY, (double)deltaX); //--- Compute angle if(BubbleAngleInArcSweep(cornerIndex, angle)) { //--- Check if in arc sweep int pixelX = (int)MathRound(centerX + deltaX); //--- Round to pixel X int pixelY = (int)MathRound(centerY + deltaY); //--- Round to pixel Y if(pixelX >= 0 && pixelX < bubbleHighResCanvas.Width() && pixelY >= 0 && pixelY < bubbleHighResCanvas.Height()) //--- Check if within bounds bubbleHighResCanvas.PixelSet(pixelX, pixelY, edgeColor); //--- Set pixel } } } }
ここでは、DrawBubbleBorder関数を実装し、高解像度Canvas上に吹き出し全体の枠線を描画します。まず、スーパーサンプリング係数を使用して線の太さと本体の角丸半径をスケーリングします。その後、吹き出しの向きに応じて分割された辺を条件付きで描画します。上向き・下向き(ORIENT_UP/ORIENT_DOWN)の場合は、上下方向の水平辺を処理します。ポインタが接続される部分では、ポインタ基部の周囲を避けるように水平辺を分割して描画し、左右の垂直辺については完全な形で描画します。左向き・右向き(ORIENT_LEFT/ORIENT_RIGHT)の場合は、水平辺を完全に描画し、垂直辺については同様にポインタ接続部分を避けるよう分割して描画します。これらの処理では、DrawBubbleHorizontalEdgeおよびDrawBubbleVerticalEdge関数を使用し、ポインタとの接続部分に枠線が重なって描画されないようにします。次に、あらかじめラジアン単位で定義した角度範囲を使用し、DrawBubbleCornerArc関数によって吹き出し本体の4つの角丸部分の円弧を描画します。続いて、3つあるポインタの辺をループ処理し、特に先端に隣接する辺を対象として、DrawBubbleStraightEdge関数を使用して接線部分から直線枠線を追加します。最後に、DrawBubbleTriangleCornerArc関数によってポインタ先端部分の円弧を描画し、枠線処理を完成させます。
辺の描画処理を統一するため、DrawBubbleHorizontalEdgeおよびDrawBubbleVerticalEdge関数では、単純にDrawBubbleStraightEdge関数を呼び出します。 DrawBubbleStraightEdgeでは、まず方向ベクトルと垂直ベクトルを計算します。その後、滑らかな接続を実現するため、端点をborderExtensionMultiplier×線の太さ分だけ延長します。次に、四角形状の帯を形成し、FillQuadrilateralを使用して辺の色で塗りつぶします。DrawBubbleCornerArcでは、内側および外側の半径を線の太さの半分から計算することで、曲線状の境界リングを作成します。その後、拡張されたピクセル領域をループ処理し、距離計算によって対象範囲内にあるピクセルを判定して描画します。また、IsAngleBetween関数によって円弧セグメント内に存在するかを確認し、正確な曲率を維持します。最後に、DrawBubbleTriangleCornerArcでは、これと同じ考え方をポインタ先端部分へ適用します。この処理では、スケーリングされた半径を使用し、ピクセルループ内でMathSqrtによって距離を計算します。さらに、MathArctan2によって角度を取得し、BubbleAngleInArcSweepによって円弧範囲内にあるかを判定します。座標にはMathRoundを使用し、境界チェックを行った後にPixelSetで描画することで、滑らかでアンチエイリアス処理された枠線を実現し、本体部分とポインタ部分が自然につながるようにします。これで、吹き出しを作成するために必要なすべてのロジックを組み合わせる準備が整いました。
void DrawBubble() { uint backgroundColorARGB = ColorToARGBWithOpacity(bubbleBackgroundColor, bubbleBackgroundOpacityPercent); //--- Get background ARGB uint borderColorARGB = ColorToARGBWithOpacity(bubbleBorderColor, bubbleBorderOpacityPercent); //--- Get border ARGB FillBubble(backgroundColorARGB); //--- Fill bubble if(bubbleShowBorder && bubbleBorderThicknessPixels > 0) //--- Check if border should be shown DrawBubbleBorder(borderColorARGB); //--- Draw bubble border BicubicDownsample(bubbleCanvas, bubbleHighResCanvas); //--- Downsample to display canvas bubbleCanvas.FontSet("Arial", 13, FW_NORMAL); //--- Set font for text string displayText = "Bubble"; //--- Set display text int textWidth, textHeight; //--- Declare text dimensions bubbleCanvas.TextSize(displayText, textWidth, textHeight); //--- Get text size int bodyDisplayLeft = (int)(bubbleBodyLeft / supersamplingFactor); //--- Compute display left int bodyDisplayTop = (int)(bubbleBodyTop / supersamplingFactor); //--- Compute display top int bodyDisplayWidth = (int)((bubbleBodyRight - bubbleBodyLeft) / supersamplingFactor); //--- Compute display width int bodyDisplayHeight = (int)((bubbleBodyBottom - bubbleBodyTop) / supersamplingFactor); //--- Compute display height int textPositionX = bodyDisplayLeft + (bodyDisplayWidth - textWidth) / 2; //--- Compute text X position int textPositionY = bodyDisplayTop + (bodyDisplayHeight - textHeight) / 2; //--- Compute text Y position bubbleCanvas.TextOut(textPositionX, textPositionY, displayText, (uint)0xFF000000, TA_LEFT); //--- Draw text on canvas } void FillBubble(uint fillColor) { FillBubbleRoundedRectangle(bubbleBodyLeft, bubbleBodyTop, bubbleBodyRight - bubbleBodyLeft, bubbleBodyBottom - bubbleBodyTop, bubbleBodyCornerRadiusPixels * supersamplingFactor, fillColor); //--- Fill bubble body rectangle FillBubbleRoundedTriangle(fillColor); //--- Fill bubble pointer triangle }
描画ロジックを統合するために、Canvas上で吹き出しの描画処理全体を制御するDrawBubble関数を定義します。まず、半透明効果を実現するために、ColorToARGBWithOpacity関数を使用して背景色と枠線の色を不透明度付きのARGB形式へ変換します。図形を構築するために、背景色を使用してFillBubble関数を呼び出し、高解像度Canvas上に吹き出し全体を塗りつぶします。その後、枠線が有効になっている場合は、DrawBubbleBorder関数を呼び出して枠線を追加します。続いて、アンチエイリアス処理された出力を得るために、BicubicDownsample関数を使用して高解像度Canvasの内容を通常解像度のCanvasへ縮小変換します。最後に、通常解像度のCanvas上でArialフォントを設定し、サイズを13、太さをFW_NORMALに指定します。その後、ラベル「Bubble」のサイズを計測し、本体の境界範囲内で中央に配置できるよう、表示座標へ変換したうえで位置を計算します。そして、識別用のラベルとして、TextOutを用いて黒色で左揃えのテキストを描画します。
FillBubble関数では、まず事前計算された座標とスケーリングされた半径を使用し、FillBubbleRoundedRectangleによって吹き出し本体を塗りつぶします。その後、FillBubbleRoundedTriangleによってポインタ部分を塗りつぶし、高解像度Canvas上で両方の図形を滑らかに統合することで、一つの吹き出し形状として完成させます。これで、初期化処理内からこの関数を呼び出し、吹き出しを描画できるようになります。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // added this for the bubble in initialization alongside the other prior shapes bubbleIsHorizontalOrientation = (bubblePointerOrientation == ORIENT_LEFT || bubblePointerOrientation == ORIENT_RIGHT); //--- Determine if orientation is horizontal int bubbleCanvasWidth, bubbleCanvasHeight; //--- Declare bubble canvas dimensions if(bubbleIsHorizontalOrientation) { //--- Check for horizontal orientation bubbleCanvasWidth = bubbleBodyWidthPixels + bubblePointerHeightPixels + 40; //--- Compute width for horizontal bubbleCanvasHeight = bubbleBodyHeightPixels + 40; //--- Compute height for horizontal } else { //--- Handle vertical orientation bubbleCanvasWidth = bubbleBodyWidthPixels + 40; //--- Compute width for vertical bubbleCanvasHeight = bubbleBodyHeightPixels + bubblePointerHeightPixels + 40; //--- Compute height for vertical } int bubblePositionY = trianglePositionY + triangleCanvasHeight + shapesGapPixels; //--- Set bubble Y position below triangle if(!bubbleCanvas.CreateBitmapLabel(0, 0, bubbleCanvasName, shapesPositionX, bubblePositionY, bubbleCanvasWidth, bubbleCanvasHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create bubble canvas bitmap label Print("Error creating bubble canvas: ", GetLastError()); //--- Print error message if creation fails return(INIT_FAILED); //--- Return initialization failure } if(!bubbleHighResCanvas.Create(bubbleCanvasName + "_hires", bubbleCanvasWidth * supersamplingFactor, bubbleCanvasHeight * supersamplingFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create high-res bubble canvas Print("Error creating bubble hi-res canvas: ", GetLastError()); //--- Print error message if creation fails return(INIT_FAILED); //--- Return initialization failure } bubbleCanvas.Erase(ColorToARGB(clrNONE, 0)); //--- Clear bubble canvas bubbleHighResCanvas.Erase(ColorToARGB(clrNONE, 0)); //--- Clear high-res bubble canvas PrecomputeBubbleGeometry(); //--- Precompute bubble geometry DrawBubble(); //--- Draw bubble bubbleCanvas.Update(); //--- Update bubble canvas display return(INIT_SUCCEEDED); //--- Return initialization success } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { rectangleHighResCanvas.Destroy(); //--- Destroy high-res rectangle canvas rectangleCanvas.Destroy(); //--- Destroy rectangle canvas ObjectDelete(0, rectangleCanvasName); //--- Delete rectangle canvas object triangleHighResCanvas.Destroy(); //--- Destroy high-res triangle canvas triangleCanvas.Destroy(); //--- Destroy triangle canvas ObjectDelete(0, triangleCanvasName); //--- Delete triangle canvas object bubbleHighResCanvas.Destroy(); //--- Destroy high-res bubble canvas bubbleCanvas.Destroy(); //--- Destroy bubble canvas ObjectDelete(0, bubbleCanvasName); //--- Delete bubble canvas object ChartRedraw(); //--- Redraw chart } //+------------------------------------------------------------------+ //| Chart event function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id == CHARTEVENT_CHART_CHANGE) { //--- Check for chart change event rectangleCanvas.Erase(ColorToARGB(clrNONE, 0)); //--- Clear rectangle canvas rectangleHighResCanvas.Erase(ColorToARGB(clrNONE, 0)); //--- Clear high-res rectangle canvas DrawRoundedRectangle(); //--- Redraw rounded rectangle rectangleCanvas.Update(); //--- Update rectangle canvas display triangleCanvas.Erase(ColorToARGB(clrNONE, 0)); //--- Clear triangle canvas triangleHighResCanvas.Erase(ColorToARGB(clrNONE, 0)); //--- Clear high-res triangle canvas DrawRoundedTriangle(); //--- Redraw rounded triangle triangleCanvas.Update(); //--- Update triangle canvas display bubbleCanvas.Erase(ColorToARGB(clrNONE, 0)); //--- Clear bubble canvas bubbleHighResCanvas.Erase(ColorToARGB(clrNONE, 0)); //--- Clear high-res bubble canvas DrawBubble(); //--- Redraw bubble bubbleCanvas.Update(); //--- Update bubble canvas display } }
OnInitイベントハンドラを拡張し、これまでの図形に加えて吹き出しの描画処理を組み込みます。まず、左向きまたは右向きのポインタであるかどうかを基準に、bubbleIsHorizontalOrientationによって水平方向の配置かどうかを判定します。その後、キャンバスサイズを適切に計算します。水平方向の場合は幅にポインタの高さを加算し、垂直方向の場合は高さにポインタの高さを加算します。また、パディングも考慮して最終的なキャンバスサイズを決定します。次に、直前のキャンバスのY座標にギャップを加えた位置を使用して、三角形の下側に吹き出しを配置します。続いて、透明度を維持するためにARGBを正規化した状態でCreateBitmapLabelを使用して通常解像度のキャンバスを作成します。また、スケーリングされたサイズを使用してCreateにより高解像度版のキャンバスを作成します。作成中にエラーが発生した場合はログへ記録し、作成に失敗した場合は初期化を失敗として終了します。描画準備として、両方のキャンバスを透明なARGBでクリアします。その後、レイアウト計算のためにPrecomputeBubbleGeometryを呼び出し、DrawBubbleによって描画を実行します。表示を更新した後、INIT_SUCCEEDEDを返します。
OnDeinitハンドラでは、終了時のクリーンアップ処理を拡張します。高解像度および通常解像度の吹き出しキャンバスを破棄し、オブジェクトを削除した後、チャートを再描画します。分かりやすくするため、今回追加した拡張部分は明示しています。
最後に、OnChartEventでは、CHARTEVENT_CHART_CHANGEイベントに対する処理へ吹き出し対応を追加します。チャート変更が発生した場合、吹き出しキャンバスをクリアし、DrawBubbleによって再描画を行います。そして、長方形および三角形の描画処理と合わせて表示を更新し、チャート変更に柔軟に対応できるようにします。コンパイルすると、次の結果が得られます。

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

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

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

結論
結論として、MQL5においてベクターベースの長方形と三角形を組み合わせることで、上、下、左、右の各方向へポインタを向けられる、向き制御機能付きの角丸吹き出しを構築しました。また、アンチエイリアス処理された描画を実現するためにスーパーサンプリングを導入し、さらにサイズ、角丸半径、枠線、不透明度などを自由に調整できるようにすることで、動的なUI要素を作成できるようにしました。これらの角丸吹き出しと向き制御機能を利用することで、今後開発するトレーディングツールにおいて、より魅力的で高度なグラフィックコンポーネントを構築するための基盤が整いました。それでは、よいトレードを。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/21271
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
流動性狩りの最適化:流動性狩りと市場構造転換の違いを見極める
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
取引規律をコードに組み込む(第1回):MQL5でライブトレードに構造的な規律を実装する
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索