English Русский Deutsch
preview
MQL5取引ツール(第13回):グラフパネルと統計パネルを備えたCCanvasベースの価格ダッシュボードの実装

MQL5取引ツール(第13回):グラフパネルと統計パネルを備えたCCanvasベースの価格ダッシュボードの実装

MetaTrader 5トレーディングシステム |
31 0
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回の記事(第12回)では、MetaQuotes Language 5 (MQL5)における相関マトリクスダッシュボードを拡張し、より高いユーザビリティを実現するためのインタラクティブ機能を追加しました。第13回では、価格変動と口座指標について、明確で実践的な判断材料を提供するよう設計された、Canvasベースの価格ダッシュボードを実装します。本ダッシュボードはCCanvasクラスを活用し、ドラッグ可能かつリサイズ可能なパネルを提供します。また、最近の価格推移を視覚的に描画し、残高、エクイティ、現在バーのOHLC (Open/High/Low/Close)などの重要な統計情報も表示します。さらに、背景カスタマイズ、テーマ切り替え、リアルタイム更新などを通じて、取引判断の精度向上を支援します。これらのツールを使用することで、市場の変化をより効率的に監視し、迅速に対応できます。本記事では以下のトピックを扱います。

  1. CCanvasベースの価格ダッシュボードフレームワークを理解する
  2. MQL5での実装
  3. バックテスト
  4. 結論

本記事では、カスタマイズ可能なMQL5 Canvasダッシュボードの完成を目指します。それでは進めます。


CCanvasベースの価格ダッシュボードフレームワークを理解する

Canvasベースの価格ダッシュボードフレームワークは、MQL5のCCanvasクラスを活用してカスタムグラフィカルパネルを作成し、リアルタイム価格データおよび口座指標を表示する仕組みです。これは、標準的なチャートインジケータの代替として機能し、メインチャートを煩雑にすることなく、状況をひと目で把握できる表示を提供するコンパクトかつインタラクティブな手法となっています。本ダッシュボードはCCanvasクラスを活用し、ドラッグ可能かつリサイズ可能なパネルを提供します。また、最近の価格推移を視覚的に描画し、残高、エクイティ、現在バーのOHLC (Open/High/Low/Close)などの口座情報を表示する統計パネルをオプションとして備えています。これらは背景画像のアルファブレンディング、グラデーションまたはソリッドフィル、そして視覚的な二重枠によって補強されます。

拡張機能としては、マウス操作によるドラッグでの再配置、枠線上へのホバーやリサイズグリップを利用したサイズ変更(アイコンによるフィードバック付き)、最小化/最大化の切り替えによるパネルの折りたたみ、ダーク/ライトテーマ切り替えによる配色の動的変更、さらに新規バーに応じたリアルタイム更新が含まれます。

これらの機能では、マウス操作のインタラクションにイベントハンドリングを使用し、画像リサイズを滑らかにするためにバイキュービック補間を採用し、フォグなどのオーバーレイ表現にはアルファブレンディングを用い、透過処理のためにARGB形式による色管理をおこないます。これにより、ダッシュボードはネイティブのMQL5オブジェクトに依存せずに柔軟にサイズ変更できるようになり、これまでに一部ネイティブオブジェクトを使用していた方針から変更し、今回はCanvas機能のみで実装することを主な目的としています。

本設計ではCanvasライブラリを組み込み、位置、サイズ、色、不透明度、描画モードの入力パラメータを定義します。背景画像リソースの読み込みとスケーリングをおこない、ヘッダー、グラフ、統計用にそれぞれ独立したCanvasを作成し、作成時のエラーチェックも実装します。また、ヘッダーではアイコン、ツールチップ、枠線を含む描画関数を実装し、グラフでは価格プロット、塗りつぶし表示、時間ラベル、リサイズ用アイコンなどを描画します。統計セクションでは、テーマ対応テキスト、グラデーション、暗色化した枠線などを表現します。さらに、色補間、色のダークニング、ブレンド処理、ARGB値の抽出などをおこなうヘルパー関数を追加します。そして、ホバー、ドラッグ、リサイズ、切り替えといったチャートイベントを処理し、最小サイズ制限を含むクランプ処理をおこないながら、インタラクションに応じて状態を更新します。最後に、ティック更新時には新しいデータに基づいて描画を更新します。以下に想定されるビジュアル表示の例を示します。

Canvasダッシュボードフレームワーク


MQL5での実装

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

//+------------------------------------------------------------------+
//|                                       Canvas Dashboard 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>

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CCanvas canvasGraph;                                             //--- Declare canvas for graph
CCanvas canvasStats;                                             //--- Declare canvas for stats
CCanvas canvasHeader;                                            //--- Declare canvas for header

string canvasGraphName = "GraphCanvas";                          //--- Set graph canvas name
string canvasStatsName = "StatsCanvas";                          //--- Set stats canvas name
string canvasHeaderName = "HeaderCanvas";                        //--- Set header canvas name

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input int graphBars                   = 50;                      // Number of recent bars to plot in the graph
input color borderColor               = clrBlack;                // Border color (change for white chart background)
input color borderHoverColor          = clrRed;                  // Border color on hover for resize indication

input int CanvasX                     = 30;                      // Main canvas X position
input int CanvasY                     = 50;                      // Main canvas Y position
input int CanvasWidth                 = 400;                     // Main canvas width
input int CanvasHeight                = 300;                     // Main canvas height

input bool EnableStatsPanel           = true;                    // Enable second stats panel
input int PanelGap                    = 10;                      // Gap between panels in pixels

input bool UseBackground              = true;                    // Enable background image
input double FogOpacity               = 0.5;                     // Fog opacity (0.0 = no fog/fully transparent, 1.0 = fully opaque)
input bool BlendFog                   = true;                    // Blend fog with image (true: image visible under fog; false: original fog hides image)

input int StatsFontSize               = 12;                      // Font size for stats panel text
input color StatsLabelColor           = clrDodgerBlue;           // Color for stats labels
input color StatsValueColor           = clrWhite;                // Color for stats values
input color StatsHeaderColor          = clrDodgerBlue;           // Color for stats headers
input int StatsHeaderFontSize         = 14;                      // Font size for stats headers

input double BorderOpacityPercentReduction = 20.0;               // Percent reduction for border opacity (0-100)
input double BorderDarkenPercent      = 30.0;                    // Percent to darken borders (0-100)

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ENUM_BACKGROUND_MODE
{
   NoColor          = 0,         // No color fill
   SingleColor      = 1,         // Single color fill
   GradientTwoColors = 2         // Gradient with two colors
};

input ENUM_BACKGROUND_MODE StatsBackgroundMode = GradientTwoColors; // Stats background mode
input color TopColor                   = clrBlack;                  // Top color for gradient or single fill
input color BottomColor                = clrRed;                    // Bottom color for gradient
input double BackgroundOpacity         = 0.7;                       // Opacity for stats background fill (0.0 to 1.0)

enum ENUM_RESIZE_MODE
{
   NONE,
   BOTTOM,
   RIGHT,
   BOTTOM_RIGHT
};

//+------------------------------------------------------------------+
//| Resources                                                        |
//+------------------------------------------------------------------+
#resource "1. Transparent MT5 bmp image.bmp"                     // Hardcoded background image resource

//+------------------------------------------------------------------+
//| Global Variables Continued                                       |
//+------------------------------------------------------------------+
uint original_bg_pixels[];                                       //--- Declare original unscaled background
uint orig_w = 0, orig_h = 0;                                     //--- Initialize original dimensions
uint bg_pixels_graph[];                                          //--- Declare scaled background for graph
uint bg_pixels_stats[];                                          //--- Declare scaled background for stats

int currentCanvasX = CanvasX;                                    //--- Set current X position
int currentCanvasY = CanvasY;                                    //--- Set current Y position
int currentWidth = CanvasWidth;                                  //--- Set current width
int currentHeight = CanvasHeight;                                //--- Set current height
bool panel_dragging = false;                                     //--- Set dragging flag
int panel_drag_x = 0, panel_drag_y = 0;                          //--- Initialize drag start mouse coordinates
int panel_start_x = 0, panel_start_y = 0;                        //--- Initialize drag start panel coordinates
bool resizing = false;                                           //--- Set resizing flag
ENUM_RESIZE_MODE resize_mode = NONE;                             //--- Set resize mode
ENUM_RESIZE_MODE hover_mode = NONE;                              //--- Set hover mode for resize
int resize_start_x = 0, resize_start_y = 0;                      //--- Initialize resize start coordinates
int start_width = 0, start_height = 0;                           //--- Initialize start dimensions
const int resize_thickness = 5;                                  //--- Set resize border thickness
const int min_width = 200;                                       //--- Set minimum width
const int min_height = 150;                                      //--- Set minimum height
int hover_mouse_local_x = 0;                                     //--- Set local mouse x for icon
int hover_mouse_local_y = 0;                                     //--- Set local mouse y for icon
bool header_hovered = false;                                     //--- Set header hover flag
bool minimize_hovered = false;                                   //--- Set minimize hover flag
bool close_hovered = false;                                      //--- Set close hover flag
bool theme_hovered = false;                                      //--- Set theme hover flag
bool resize_hovered = false;                                     //--- Set resize hover flag
int prev_mouse_state = 0;                                        //--- Initialize previous mouse state
int last_mouse_x = 0, last_mouse_y = 0;                          //--- Initialize last mouse position
int header_height = 27;                                          //--- Set header height
int gap_y = 7;                                                   //--- Set y gap
int button_size = 25;                                            //--- Set button size
int theme_x_offset = -75;                                        //--- Set theme offset relative to header right
int minimize_x_offset = -50;                                     //--- Set minimize offset relative to header right
int close_x_offset = -25;                                        //--- Set close offset relative to header right
bool panels_minimized = false;                                   //--- Set minimized flag
color HeaderColor = C'60,60,60';                                 //--- Set header color
color HeaderHoverColor = clrRed;                                 //--- Set header hover color
color HeaderDragColor = clrMediumBlue;                           //--- Set header drag color

bool is_dark_theme = true;                                       //--- Set dark theme flag

color LightHeaderColor = clrSilver;                              //--- Set light header color
color LightHeaderTextColor = clrBlack;                           //--- Set light header text color
color LightStatsLabelColor = clrBlue;                            //--- Set light stats label color
color LightStatsValueColor = clrBlack;                           //--- Set light stats value color
color LightStatsHeaderColor = clrBlue;                           //--- Set light stats header color
color LightBorderColor = clrBlack;                               //--- Set light border color
color LightTopColor = clrGreen;                                  //--- Set light top color
color LightBottomColor = clrGold;                                //--- Set light bottom color
color LightHeaderHoverColor = clrRed;                            //--- Set light header hover color
color LightHeaderDragColor = clrMediumBlue;                      //--- Set light header drag color

bool graphCreated = false;                                       //--- Set graph created flag
bool statsCreated = false;                                       //--- Set stats created flag

実装は、#include <Canvas/Canvas.mqh>によるCanvasライブラリの組み込みから開始します。このライブラリは、チャート上でビットマップベースのグラフィカルパネルを作成および管理するためのCCanvasクラスを提供します。続いて、3つのCCanvasオブジェクトを宣言します。canvasGraphは価格グラフパネル用、canvasStatsは統計パネル用、canvasHeaderはヘッダーセクション用です。また、それぞれを一意に識別するため、文字列定数としてGraphCanvas、StatsCanvas、HeaderCanvasを設定します。

次にダッシュボードをカスタマイズするための入力パラメータを定義します。具体的には描画するバー数を指定するgraphBarsを50、枠線色を黒とするborderColor、ホバー時の枠線色を赤とするborderHoverColorを設定します。また位置およびサイズとしてCanvasXを30、CanvasYを50、CanvasWidthを400、CanvasHeightを300に設定します。さらに統計パネルを表示するためのブール値EnableStatsPanelをtrueとし、パネル間の間隔を指定するPanelGapを10ピクセルに設定します。背景画像を使用するためのUseBackgroundをtrue、フォグ効果の不透明度を指定するFogOpacityを0.5、フォグをブレンドするためのBlendFogをtrueに設定します。統計表示関連ではStatsFontSizeを12、StatsLabelColorをドジャーブルーに設定します。また枠線調整用のパラメータとしてBorderOpacityPercentReductionを20.0、BorderDarkenPercentを30.0に設定します。

続いて背景描画モードを定義するENUM_BACKGROUND_MODE列挙型を作成します。この列挙型には塗りつぶしをおこなわないNoColor、単色塗りつぶしをおこなうSingleColor、2色グラデーションを適用するGradientTwoColorsの各オプションを用意します。入力パラメータStatsBackgroundModeのデフォルト値はグラデーションとし、グラデーション用の色としてTopColorを黒、BottomColorを赤に設定します。また背景の不透明度を指定するBackgroundOpacityは0.7に設定します。その後リサイズ状態を管理するためのENUM_RESIZE_MODE列挙型を追加します。この列挙型にはリサイズなしを表すNONE、下辺リサイズのBOTTOM、右辺リサイズのRIGHT、右下コーナーのBOTTOM_RIGHTを定義します。

#resourceディレクティブを使用してリソースを組み込みます。ここではハードコードされた背景画像として「1.Transparent MetaTrader 5 bmp image.bmp」を指定します。添付する画像はビットマップファイルのみ対応しています。これは簡単に対応可能です。現在の画像ファイルはこのような状態になっています。簡素化のため、プログラムファイルと同じ場所にインポートしました。以下に実際の内容をご確認ください。

ビットマップ画像ファイル

画像ファイルの準備が完了したため、次にグローバル変数の定義に進みます。画像ピクセル用の配列としてoriginal_bg_pixels、bg_pixels_graph、bg_pixels_statsを用意します。また画像の元サイズを保持するためにorig_wとorig_hを0で初期化します。現在の位置およびサイズは入力パラメータから初期化し、ドラッグ状態を管理するpanel_draggingはfalse、リサイズ状態を管理するresizingもfalseとします。さらにresize_modeおよびhover_modeの列挙値はともにNONEに設定します。リサイズ処理用の整数として、開始位置や最小制限を管理する変数を定義します。たとえばresize_thicknessは5、min_widthは200とします。ホバー状態としてheader_hoveredはfalse、前回のマウス状態を保持するprev_mouse_stateは0で初期化します。レイアウト定数としてheader_heightは27、gap_yは7、button_sizeは25を設定し、ボタン位置調整用のオフセットも定義します。パネルの最小化状態を示すpanels_minimizedはfalseとします。カラー設定としてHeaderColorはミディアムグレー、HeaderHoverColorは赤、テーマフラグis_dark_themeはtrueに設定します。ライトモード用カラーとしてLightHeaderColorはシルバーを定義します。さらに初期化状態を示すフラグとしてgraphCreatedおよびstatsCreatedをfalseに設定します。

これらの準備が完了したため、次に画像を新しいサイズに合わせて動的にスケーリングする関数を作成します。この処理は、パネルのリサイズ時に背景画像を適切に拡縮するために使用されます。以下がそのロジックです。

//+------------------------------------------------------------------+
//| Scale image                                                      |
//+------------------------------------------------------------------+
void ScaleImage(uint &pixels[], int original_width, int original_height, int new_width, int new_height) {
   uint scaled_pixels[];                                         //--- Declare scaled array
   ArrayResize(scaled_pixels, new_width * new_height);           //--- Resize scaled
   for (int y = 0; y < new_height; y++)                          //--- Loop new rows
   {
      for (int x = 0; x < new_width; x++)                        //--- Loop new columns
      {
         double original_x = (double)x * original_width / new_width; //--- Compute original x
         double original_y = (double)y * original_height / new_height; //--- Compute original y
         uint pixel = BicubicInterpolate(pixels, original_width, original_height, original_x, original_y); //--- Interpolate pixel
         scaled_pixels[y * new_width + x] = pixel;               //--- Set scaled pixel
      }
   }
   ArrayResize(pixels, new_width * new_height);                  //--- Resize original
   ArrayCopy(pixels, scaled_pixels);                             //--- Copy scaled
}

//+------------------------------------------------------------------+
//| Bicubic interpolate pixel                                        |
//+------------------------------------------------------------------+
uint BicubicInterpolate(uint &pixels[], int width, int height, double x, double y) {
   int x0 = (int)x;                                              //--- Get integer x
   int y0 = (int)y;                                              //--- Get integer y
   double fractional_x = x - x0;                                 //--- Get fractional x
   double fractional_y = y - y0;                                 //--- Get fractional y
   int x_indices[4], y_indices[4];                               //--- Declare indices
   for (int i = -1; i <= 2; i++)                                 //--- Loop offsets
   {
      x_indices[i + 1] = MathMin(MathMax(x0 + i, 0), width - 1); //--- Clamp x index
      y_indices[i + 1] = MathMin(MathMax(y0 + i, 0), height - 1); //--- Clamp y index
   }
   uint neighborhood_pixels[16];                                 //--- Declare neighborhood
   for (int j = 0; j < 4; j++)                                   //--- Loop y indices
   {
      for (int i = 0; i < 4; i++)                                //--- Loop x indices
      {
         neighborhood_pixels[j * 4 + i] = pixels[y_indices[j] * width + x_indices[i]]; //--- Get pixel
      }
   }
   uchar alpha_components[16], red_components[16], green_components[16], blue_components[16]; //--- Declare components
   for (int i = 0; i < 16; i++)                                  //--- Loop pixels
   {
      GetArgb(neighborhood_pixels[i], alpha_components[i], red_components[i], green_components[i], blue_components[i]); //--- Get ARGB
   }
   uchar alpha_out = (uchar)BicubicInterpolateComponent(alpha_components, fractional_x, fractional_y); //--- Interpolate alpha
   uchar red_out = (uchar)BicubicInterpolateComponent(red_components, fractional_x, fractional_y); //--- Interpolate red
   uchar green_out = (uchar)BicubicInterpolateComponent(green_components, fractional_x, fractional_y); //--- Interpolate green
   uchar blue_out = (uchar)BicubicInterpolateComponent(blue_components, fractional_x, fractional_y); //--- Interpolate blue
   return (((uint)alpha_out) << 24) | (((uint)red_out) << 16) | (((uint)green_out) << 8) | ((uint)blue_out); //--- Return interpolated
}

//+------------------------------------------------------------------+
//| Bicubic interpolate component                                    |
//+------------------------------------------------------------------+
double BicubicInterpolateComponent(uchar &components[], double fractional_x, double fractional_y) {
   double weights_x[4];                                          //--- Declare x weights
   double t = fractional_x;                                      //--- Set t x
   weights_x[0] = (-0.5 * t * t * t + t * t - 0.5 * t);          //--- Compute weight 0
   weights_x[1] = (1.5 * t * t * t - 2.5 * t * t + 1);           //--- Compute weight 1
   weights_x[2] = (-1.5 * t * t * t + 2 * t * t + 0.5 * t);      //--- Compute weight 2
   weights_x[3] = (0.5 * t * t * t - 0.5 * t * t);               //--- Compute weight 3
   double y_values[4];                                           //--- Declare y values
   for (int j = 0; j < 4; j++)                                   //--- Loop y
   {
      y_values[j] = weights_x[0] * components[j * 4 + 0] + weights_x[1] * components[j * 4 + 1] + weights_x[2] * components[j * 4 + 2] + weights_x[3] * components[j * 4 + 3]; //--- Compute y value
   }
   double weights_y[4];                                          //--- Declare y weights
   t = fractional_y;                                             //--- Set t y
   weights_y[0] = (-0.5 * t * t * t + t * t - 0.5 * t);          //--- Compute weight 0
   weights_y[1] = (1.5 * t * t * t - 2.5 * t * t + 1);           //--- Compute weight 1
   weights_y[2] = (-1.5 * t * t * t + 2 * t * t + 0.5 * t);      //--- Compute weight 2
   weights_y[3] = (0.5 * t * t * t - 0.5 * t * t);               //--- Compute weight 3
   double result = weights_y[0] * y_values[0] + weights_y[1] * y_values[1] + weights_y[2] * y_values[2] + weights_y[3] * y_values[3]; //--- Compute result
   return MathMax(0, MathMin(255, result));                      //--- Clamp result
}

//+------------------------------------------------------------------+
//| Get ARGB components                                              |
//+------------------------------------------------------------------+
void GetArgb(uint pixel, uchar &alpha, uchar &red, uchar &green, uchar &blue) {
   alpha = (uchar)((pixel >> 24) & 0xFF);                        //--- Get alpha
   red = (uchar)((pixel >> 16) & 0xFF);                          //--- Get red
   green = (uchar)((pixel >> 8) & 0xFF);                         //--- Get green
   blue = (uchar)(pixel & 0xFF);                                 //--- Get blue
}

まずScaleImage関数を実装します。この関数は、元の画像のピクセル配列を新しい幅と高さにリサイズするもので、滑らかなスケーリングのためにバイキュービック補間を使用します。引数としてピクセル配列の参照、元の幅と高さ、そして新しい幅と高さを受け取ります。scaled_pixelsという一時配列を新しいサイズで宣言しリサイズします。その後、新しい高さと幅に対してネストしたループをおこない、各ピクセルについて元画像上の対応座標を比例マッピングで計算します。各新規ピクセルごとにBicubicInterpolate関数を呼び出し、元のピクセル配列と小数座標を渡して補間値を取得し、それをscaled_pixelsに格納します。最後に入力のpixels配列を新しいサイズにリサイズし、ArrayCopy関数を用いてscaled_pixelsの内容をコピーします。

次にBicubicInterpolate関数について説明します。この関数は指定された小数座標xおよびyにおける単一ピクセル値をバイキュービック補間で計算します。引数としてpixels配列、width、height、およびdouble型のxとyを受け取ります。まずx0およびy0として整数部分を取得し、小数部分を算出します。その後、4×4近傍を構成するために-1から2のオフセット範囲でインデックス配列を作成し、MathMinおよびMathMaxを用いて画像境界内にクランプします。続いて16個の近傍ピクセルを配列に抽出します。次にアルファ、レッド、グリーン、ブルーの各コンポーネント用配列を用意し、GetArgb関数を使ってループ処理で各ピクセルから成分を取り出し格納します。その後、それぞれのカラーチャンネルに対してBicubicInterpolateComponent関数を呼び出し、小数座標を用いて補間計算をおこないます。得られた値はucharにキャストされ、ビットシフトを用いてuint型のARGB値として再構成されます。

次にBicubicInterpolateComponent関数を作成し、単一カラーチャンネルの4×4コンポーネント配列に対して、小数xおよびyを用いたバイキュービック補間を実行します。この関数はまずfractional xに基づいてバイキュービックカーネルの式から4つのx方向の重みを計算します。次にコンポーネント配列の各行についてこれらの重みを用いて加重和を取り、4つの中間y値を算出します。同様にfractional yを用いてy方向の重みを計算し、それら中間値に対して再び加重和をおこなうことで最終結果を得ます。結果は0から255の範囲にMathMaxおよびMathMinでクランプされます。最後にGetArgb関数を実装し、uint型のピクセル値からARGB各成分をuchar参照として抽出します。具体的には24ビット、16ビット、8ビット、0ビットの右シフトと0xFFマスクを用いてアルファ、レッド、グリーン、ブルーを取得します。

なお重要な点として、必ずしもバイキュービックアプローチを使用する必要はありません。線形補間やバイリニア補間を使用することも可能ですが、その場合は私たちが意図するものよりもギザギザになります。そのため、アンチエイリアスを考慮した画像ピクセレーションとして最も適した手法を採用しています。実際には以下のように複数の補間手法が存在します。

異なる補間方法

バイキュービック補間は、他の手法と比較して最も滑らかな画像表現を実現することが確認できます。次におこなう処理として、これらの関数を用いてリソース画像をリサイズし、動的に適合させます。現在の画像ファイルは、描画したいCanvas領域よりもかなり大きいため、リサイズが必要です。それでは初期化イベントハンドラ内で実装します。

//+------------------------------------------------------------------+
//| Initialize expert                                                |
//+------------------------------------------------------------------+
int OnInit()
{
   currentWidth = CanvasWidth;                                   //--- Set initial width
   currentHeight = CanvasHeight;                                 //--- Set initial height
   currentCanvasX = CanvasX;                                     //--- Set initial X
   currentCanvasY = CanvasY;                                     //--- Set initial Y
   
   if (UseBackground)                                            //--- Check if background enabled
   {
      if (ResourceReadImage("::1. Transparent MT5 bmp image.bmp", original_bg_pixels, orig_w, orig_h) && orig_w > 0 && orig_h > 0) //--- Load image if valid
      {
         ArrayCopy(bg_pixels_graph, original_bg_pixels);         //--- Copy to graph background
         ScaleImage(bg_pixels_graph, (int)orig_w, (int)orig_h, currentWidth, currentHeight); //--- Scale graph background
         if (EnableStatsPanel)                                   //--- Check stats panel
         {
            ArrayCopy(bg_pixels_stats, original_bg_pixels);      //--- Copy to stats background
            ScaleImage(bg_pixels_stats, (int)orig_w, (int)orig_h, currentWidth / 2, currentHeight); //--- Scale stats background
         }
      }
      else                                                       //--- Handle load failure
      {
         Print("Failed to load background image from ::1. Transparent MT5 bmp image.bmp"); //--- Print error
      }
   }
   
   int header_width = currentWidth + (EnableStatsPanel ? PanelGap + currentWidth / 2 : 0); //--- Compute header width
   if (!canvasHeader.CreateBitmapLabel(0, 0, canvasHeaderName, currentCanvasX, currentCanvasY, header_width, header_height, COLOR_FORMAT_ARGB_NORMALIZE)) //--- Create header canvas
   {
      Print("Failed to create Header Canvas");                   //--- Print error
      return(INIT_FAILED);                                       //--- Return failure
   }
   
   if (!canvasGraph.CreateBitmapLabel(0, 0, canvasGraphName, currentCanvasX, currentCanvasY + header_height + gap_y, currentWidth, currentHeight, COLOR_FORMAT_ARGB_NORMALIZE)) //--- Create graph canvas
   {
      Print("Failed to create Graph Canvas");                    //--- Print error
      return(INIT_FAILED);                                       //--- Return failure
   }
   graphCreated = true;                                          //--- Set graph created
   
   if (EnableStatsPanel)                                         //--- Check stats panel
   {
      int statsX = currentCanvasX + currentWidth + PanelGap;     //--- Compute stats X
      if (!canvasStats.CreateBitmapLabel(0, 0, canvasStatsName, statsX, currentCanvasY + header_height + gap_y, currentWidth / 2, currentHeight, COLOR_FORMAT_ARGB_NORMALIZE)) //--- Create stats canvas
      {
         Print("Failed to create Stats Canvas");                 //--- Print error
      }
      statsCreated = true;                                       //--- Set stats created
   }
   
   ChartRedraw();                                                //--- Redraw chart
   return(INIT_SUCCEEDED);                                       //--- Return success
}

OnInitイベントハンドラ内では、初期状態のセットアップをおこない、ダッシュボード用のCanvasパネルを作成します。まずcurrentWidthをCanvasWidthへ、currentHeightをCanvasHeightへ、currentCanvasXをCanvasXへ、currentCanvasYをCanvasYへといった入力値から現在の寸法および位置を初期化します。UseBackgroundがtrueの場合、ResourceReadImageを用いてリソース画像をoriginal_bg_pixelsに読み込み、元の幅と高さを取得します。取得に成功した場合はbg_pixels_graphへコピーし、ScaleImage関数を使用して現在のサイズにスケーリングします。統計パネルについてEnableStatsPanelがtrueの場合、bg_pixels_statsへコピーし、幅は半分、高さはフルサイズとしてスケーリングします。読み込みに失敗した場合はエラーメッセージを出力します。

次にヘッダー幅をグラフ幅と任意の統計幅およびギャップから算出し、CreateBitmapLabelを使用してサブウィンドウ0にヘッダーCanvasを作成します。パラメータとしてcanvasHeaderName、位置、幅、およびheader_heightを指定し、COLOR_FORMAT_ARGB_NORMALIZE 形式で生成します。作成に失敗した場合はエラーメッセージを出力しINIT_FAILEDを返します。同様にグラフCanvasをヘッダーの下にgap_y分オフセットして作成し、成功時にはgraphCreatedをtrueに設定し、失敗時にはINIT_FAILED を返します。統計パネルが有効な場合は、グラフの右側にPanelGapを加えた位置をx座標として計算し、統計Canvasを作成しstatsCreatedをtrueに設定します。最後にChartRedrawを呼び出してチャートを再描画し、INIT_SUCCEEDEDを返します。これにより実際の描画前段階としてCanvasの描画領域が生成されます。これを実証するために、コンパイル時にツールチップに表示される内容の例を以下に示します。

Canvasエリア

コンパイル後のツールチップ上では、Canvas領域が正しく描画されていることが確認できます。次におこなうべきは実際の描画処理であり、Canvasオブジェクトを描画していきます。その前にヘッダー描画から開始しますが、テーマ対応が必要なため、まずはダークテーマおよびライトテーマの描画に対応するためのテーマヘルパー関数を定義します。

//+------------------------------------------------------------------+
//| Get theme-aware colors                                           |
//+------------------------------------------------------------------+
color GetHeaderColor() { return is_dark_theme ? HeaderColor : LightHeaderColor; }                //--- Return header color
color GetHeaderHoverColor() { return is_dark_theme ? HeaderHoverColor : LightHeaderHoverColor; } //--- Return hover color
color GetHeaderDragColor() { return is_dark_theme ? HeaderDragColor : LightHeaderDragColor; }    //--- Return drag color
color GetStatsLabelColor() { return is_dark_theme ? StatsLabelColor : LightStatsLabelColor; }    //--- Return label color
color GetStatsValueColor() { return is_dark_theme ? StatsValueColor : LightStatsValueColor; }    //--- Return value color
color GetStatsHeaderColor() { return is_dark_theme ? StatsHeaderColor : LightStatsHeaderColor; } //--- Return header color
color GetBorderColor() { return is_dark_theme ? borderColor : LightBorderColor; }                //--- Return border color
color GetTopColor() { return is_dark_theme ? TopColor : LightTopColor; }                         //--- Return top color
color GetBottomColor() { return is_dark_theme ? BottomColor : LightBottomColor; }                //--- Return bottom color
color GetHeaderTextColor() { return is_dark_theme ? clrWhite : LightHeaderTextColor; }           //--- Return text color
color GetIconColor(bool is_drag) { return is_drag ? GetHeaderDragColor() : GetHeaderHoverColor(); } //--- Return icon color

ここでは、現在のis_dark_themeフラグに基づいてテーマ対応カラーを取得するための複数のgetter関数を実装します。これにより、ダークモードとライトモードの両方で一貫したビジュアルを維持しつつ、他の箇所で冗長な条件分岐をおこなう必要がなくなります。GetHeaderColor関数は、ヘッダー背景用の色として、ダークモードの場合はHeaderColorを返し、ライトモードの場合はLightHeaderColorを返します。同様のロジックを他のすべてのgetter関数にも適用します。これらのヘルパー関数を用いることで、ヘッダーCanvasオブジェクトの生成処理を簡潔に記述できるようになります。

//+------------------------------------------------------------------+
//| Draw header on header canvas                                     |
//+------------------------------------------------------------------+
void DrawHeaderOnCanvas()
{
   canvasHeader.Erase(0);                                        //--- Clear canvas
   
   color header_bg = panel_dragging ? GetHeaderDragColor() : (header_hovered ? GetHeaderHoverColor() : GetHeaderColor()); //--- Set background
   uint argb_bg = ColorToARGB(header_bg, 255);                   //--- Convert to ARGB
   canvasHeader.FillRectangle(0, 0, canvasHeader.Width() - 1, header_height - 1, argb_bg); //--- Fill background
   
   uint argbBorder = ColorToARGB(GetBorderColor(), 255);         //--- Convert border to ARGB
   canvasHeader.Line(0, 0, canvasHeader.Width() - 1, 0, argbBorder); //--- Draw top border
   canvasHeader.Line(canvasHeader.Width() - 1, 0, canvasHeader.Width() - 1, header_height - 1, argbBorder); //--- Draw right border
   canvasHeader.Line(canvasHeader.Width() - 1, header_height - 1, 0, header_height - 1, argbBorder); //--- Draw bottom border
   canvasHeader.Line(0, header_height - 1, 0, 0, argbBorder);    //--- Draw left border
   
   canvasHeader.FontSet("Arial Bold", 15);                       //--- Set font
   uint argbText = ColorToARGB(GetHeaderTextColor(), 255);       //--- Convert text to ARGB
   canvasHeader.TextOut(10, (header_height - 15) / 2, "Price Dashboard", argbText, TA_LEFT); //--- Draw title
   
   int theme_x = canvasHeader.Width() + theme_x_offset;          //--- Compute theme x
   string theme_symbol = CharToString((uchar)91);                //--- Set theme symbol
   color theme_color = theme_hovered ? clrYellow : GetHeaderTextColor(); //--- Set theme color
   canvasHeader.FontSet("Wingdings", 22);                        //--- Set font
   uint argb_theme = ColorToARGB(theme_color, 255);              //--- Convert to ARGB
   canvasHeader.TextOut(theme_x, (header_height - 22) / 2, theme_symbol, argb_theme, TA_CENTER); //--- Draw theme icon
   
   int min_x = canvasHeader.Width() + minimize_x_offset;         //--- Compute minimize x
   string min_symbol = panels_minimized ? CharToString((uchar)111) : CharToString((uchar)114); //--- Set minimize symbol
   color min_color = minimize_hovered ? clrYellow : GetHeaderTextColor(); //--- Set minimize color
   canvasHeader.FontSet("Wingdings", 22);                        //--- Set font
   uint argb_min = ColorToARGB(min_color, 255);                  //--- Convert to ARGB
   canvasHeader.TextOut(min_x, (header_height - 22) / 2, min_symbol, argb_min, TA_CENTER); //--- Draw minimize icon
   
   int close_x = canvasHeader.Width() + close_x_offset;          //--- Compute close x
   string close_symbol = CharToString((uchar)114);               //--- Set close symbol
   color close_color = close_hovered ? clrRed : GetHeaderTextColor(); //--- Set close color
   canvasHeader.FontSet("Webdings", 22);                         //--- Set font
   uint argb_close = ColorToARGB(close_color, 255);              //--- Convert to ARGB
   canvasHeader.TextOut(close_x, (header_height - 22) / 2, close_symbol, argb_close, TA_CENTER); //--- Draw close icon
   
   canvasHeader.Update();                                        //--- Update canvas
}

ここでは、DrawHeaderOnCanvas関数を実装し、canvasHeaderオブジェクト上にヘッダーセクションを描画します。この処理ではタイトルおよびインタラクティブアイコンを描画し、ドラッグやホバーなどの状態に応じて動的に色を変更します。まずEraseメソッドを使用してCanvasをゼロでクリアします。次に背景色を条件分岐で決定します。panel_draggingがtrueの場合はGetHeaderDragColorを使用し、header_hoveredがtrueの場合はGetHeaderHoverColorを使用し、それ以外の場合はGetHeaderColorを使用します。取得した色はColorToARGBでアルファ255のARGB値に変換し、(0,0)から(width - 1, header_height - 1)の矩形をFillRectangleで塗りつぶします。

次に枠線を描画します。GetBorderColorで取得した色をARGB(アルファ255)に変換し、Lineメソッドを用いてヘッダーの上下左右の各辺を描画します。フォントはArial Boldのサイズ15にFontSetで設定し、ヘッダーテキスト色をGetHeaderTextColorで取得してARGBに変換します。その後、タイトルとしてPrice Dashboardをx=10の位置に、縦方向中央揃え、左揃えでTextOutを用いて描画します。

テーマアイコンについては、x座標をCanvas幅にtheme_x_offsetを加算して計算します。シンボルはucharキャストした文字コード91を使用します。色はtheme_hoveredがtrueの場合は黄色、それ以外はヘッダーテキスト色とします。フォントはWingdingsのサイズ22に変更し、ARGB変換後、中央揃えでTextOutにより描画します。同様に最小化アイコンではx座標をminimize_x_offsetで計算し、panels_minimizedがtrueの場合は文字コード111、それ以外は114を使用します。ホバー時は黄色、それ以外は通常のテキスト色を使用し、Wingdingsフォントで中央配置して描画します。クローズアイコンではx座標をclose_x_offsetで計算し、シンボルは常に114を使用します。close_hoveredがtrueの場合は赤色、それ以外はテキスト色を使用し、Webdingsフォントのサイズ22で中央揃えに描画します。最後にUpdateメソッドを呼び出してCanvasを更新し、変更を画面に反映させます。この関数を初期化時に呼び出すことで、以下のような結果が得られます。

CANVASヘッダー描画

画像から確認できるように、Canvasヘッダーは正しくラベル付けされています。次におこなうべきはグラフCanvas領域への描画であり、価格の分析結果をラインチャートとして描画します。本質的にはシンプルな価格グラフを自前で構築する処理です。ただしこれはあくまで今回のデモ用として最も簡単な実装を選択しているものであり、必要に応じてより複雑な計算ロジックに置き換えることも可能です。以下がその実装方針です。

//+------------------------------------------------------------------+
//| Update the price graph on the main Canvas                        |
//+------------------------------------------------------------------+
void UpdateGraphOnCanvas()
{
   canvasGraph.Erase(0);                                         //--- Clear canvas
   
   if (UseBackground && ArraySize(bg_pixels_graph) == currentWidth * currentHeight) //--- Check background
   {
      for (int y = 0; y < currentHeight; y++)                    //--- Loop rows
      {
         for (int x = 0; x < currentWidth; x++)                  //--- Loop columns
         {
            canvasGraph.PixelSet(x, y, bg_pixels_graph[y * currentWidth + x]); //--- Set pixel
         }
      }
   }
   
   uint argbBorder = ColorToARGB(GetBorderColor(), 255);         //--- Convert border to ARGB
   canvasGraph.Line(0, 0, currentWidth - 1, 0, argbBorder);      //--- Draw top outer
   canvasGraph.Line(currentWidth - 1, 0, currentWidth - 1, currentHeight - 1, argbBorder); //--- Draw right outer
   canvasGraph.Line(currentWidth - 1, currentHeight - 1, 0, currentHeight - 1, argbBorder); //--- Draw bottom outer
   canvasGraph.Line(0, currentHeight - 1, 0, 0, argbBorder);     //--- Draw left outer
   canvasGraph.Line(1, 1, currentWidth - 2, 1, argbBorder);      //--- Draw top inner
   canvasGraph.Line(currentWidth - 2, 1, currentWidth - 2, currentHeight - 2, argbBorder); //--- Draw right inner
   canvasGraph.Line(currentWidth - 2, currentHeight - 2, 1, currentHeight - 2, argbBorder); //--- Draw bottom inner
   canvasGraph.Line(1, currentHeight - 2, 1, 1, argbBorder);     //--- Draw left inner
   
   double closePrices[];                                         //--- Declare close array
   ArrayResize(closePrices, graphBars);                          //--- Resize close
   if (CopyClose(_Symbol, _Period, 0, graphBars, closePrices) != graphBars) //--- Copy closes
   {
      Print("Failed to copy close prices");                      //--- Print error
      return;                                                    //--- Exit
   }
   
   datetime timeArr[];                                           //--- Declare time array
   ArrayResize(timeArr, graphBars);                              //--- Resize time
   if (CopyTime(_Symbol, _Period, 0, graphBars, timeArr) != graphBars) //--- Copy times
   {
      Print("Failed to copy times");                             //--- Print error
      return;                                                    //--- Exit
   }
   
   double minPrice = closePrices[0];                             //--- Set initial min
   double maxPrice = closePrices[0];                             //--- Set initial max
   for (int i = 1; i < graphBars; i++)                           //--- Loop prices
   {
      if (closePrices[i] < minPrice) minPrice = closePrices[i];  //--- Update min
      if (closePrices[i] > maxPrice) maxPrice = closePrices[i];  //--- Update max
   }
   double priceRange = maxPrice - minPrice;                      //--- Compute range
   if (priceRange == 0) priceRange = _Point;                     //--- Avoid zero
   
   int graphLeft = 2;                                            //--- Set left margin
   int graphRight = currentWidth - 3;                            //--- Set right margin
   double graphWidth_d = graphRight - graphLeft;                 //--- Compute width
   int graphHeight = currentHeight - 4;                          //--- Compute height
   int bottomY = 2 + graphHeight;                                //--- Set bottom y
   
   int x_pos[];                                                  //--- Declare x positions
   int y_pos[];                                                  //--- Declare y positions
   ArrayResize(x_pos, graphBars);                                //--- Resize x
   ArrayResize(y_pos, graphBars);                                //--- Resize y
   for (int i = 0; i < graphBars; i++)                           //--- Loop bars
   {
      double norm = (graphBars > 1) ? (double)i / (graphBars - 1) : 0.0; //--- Normalize
      x_pos[i] = graphLeft + (int)(norm * graphWidth_d + 0.5);   //--- Set x
      double price = closePrices[graphBars - 1 - i];             //--- Get price (flipped)
      y_pos[i] = 2 + (int)(graphHeight * (maxPrice - price) / priceRange + 0.5); //--- Set y
   }
   
   color lineColor = clrBlue;                                    //--- Set line color
   uint argbLine = ColorToARGB(lineColor, 255);                  //--- Convert to ARGB
   for (int i = 0; i < graphBars - 1; i++)                       //--- Loop segments
   {
      int x1 = (currentWidth - 1) - x_pos[i];                    //--- Set x1 (flipped)
      int y1 = y_pos[i];                                         //--- Set y1
      int x2 = (currentWidth - 1) - x_pos[i + 1];                //--- Set x2 (flipped)
      int y2 = y_pos[i + 1];                                     //--- Set y2
      canvasGraph.LineAA(x1, y1, x2, y2, argbLine);              //--- Draw line
   }
   
   int min_flipped_x = (currentWidth - 1) - graphRight;          //--- Set min flipped x
   int max_flipped_x = (currentWidth - 1) - graphLeft;           //--- Set max flipped x
   for (int colX = min_flipped_x; colX <= max_flipped_x; colX++) //--- Loop columns
   {
      int logical_colX = (currentWidth - 1) - colX;              //--- Get logical x
      int seg = -1;                                              //--- Initialize segment
      for (int j = 0; j < graphBars - 1; j++)                    //--- Loop segments
      {
         if (x_pos[j] <= logical_colX && logical_colX <= x_pos[j + 1]) //--- Check segment
         {
            seg = j;                                             //--- Set segment
            break;                                               //--- Exit
         }
      }
      if (seg == -1) continue;                                   //--- Skip if no segment
      
      double dx = x_pos[seg + 1] - x_pos[seg];                   //--- Compute dx
      double t = (dx > 0) ? (logical_colX - x_pos[seg]) / dx : 0.0; //--- Compute t
      double interpY = y_pos[seg] + t * (y_pos[seg + 1] - y_pos[seg]); //--- Interpolate y
      int topY = (int)(interpY + 0.5);                           //--- Round top y
      
      for (int fillY = topY; fillY < bottomY; fillY++)           //--- Loop fill
      {
         double fadeFactor = (double)(bottomY - fillY) / (bottomY - topY); //--- Compute fade
         uchar alpha = (uchar)(255 * fadeFactor * FogOpacity);   //--- Compute alpha
         uint argbFill = ColorToARGB(lineColor, alpha);          //--- Convert fill
         if (BlendFog)                                           //--- Check blend
         {
            uint currentPixel = canvasGraph.PixelGet(colX, fillY); //--- Get pixel
            uint blendedPixel = BlendPixels(currentPixel, argbFill); //--- Blend pixels
            canvasGraph.PixelSet(colX, fillY, blendedPixel);     //--- Set blended
         }
         else                                                    //--- Handle no blend
         {
            canvasGraph.PixelSet(colX, fillY, argbFill);         //--- Set fill
         }
      }
   }
   
   canvasGraph.FontSet("Arial", 12);                             //--- Set font
   uint argbText = ColorToARGB(is_dark_theme ? clrBlack : clrGray, 255); //--- Convert text
   canvasGraph.TextOut(currentWidth / 2, 10, "Price Graph (" + _Symbol + ")", argbText, TA_CENTER); //--- Draw title
   
   canvasGraph.FontSet("Arial", 12);                             //--- Set font
   
   string newTime = TimeToString(timeArr[0], TIME_DATE | TIME_MINUTES); //--- Get new time
   string oldTime = TimeToString(timeArr[graphBars - 1], TIME_DATE | TIME_MINUTES); //--- Get old time
   canvasGraph.TextOut(10, currentHeight - 15, newTime, argbText, TA_LEFT); //--- Draw new time
   canvasGraph.TextOut(currentWidth - 10, currentHeight - 15, oldTime, argbText, TA_RIGHT); //--- Draw old time
   
   if (resize_hovered || resizing)                               //--- Check resize state
   {
      ENUM_RESIZE_MODE active_mode = resizing ? resize_mode : hover_mode; //--- Get active mode
      if (active_mode == NONE)                                       //--- Check none
      {
         canvasGraph.Update();                                   //--- Update canvas
         return;                                                 //--- Exit
      }
      
      string icon_font = "Wingdings 3";                          //--- Set icon font
      int icon_size = 25;                                        //--- Set icon size
      uchar icon_code;                                           //--- Declare code
      int angle = 0;                                             //--- Set angle
      switch (active_mode)                                       //--- Switch mode
      {
         case BOTTOM: 
            icon_code = (uchar)'2';                              //--- Set bottom code
            angle = 0;                                           //--- Set angle
            break;
         case RIGHT: 
            icon_code = (uchar)'1';                              //--- Set right code
            angle = 0;                                           //--- Set angle
            break;
         case BOTTOM_RIGHT: 
            icon_code = (uchar)'2';                              //--- Set corner code
            angle = 450;                                         //--- Set angle
            break;
         default: canvasGraph.Update(); return;
      }
      string icon_symbol = CharToString(icon_code);              //--- Set symbol
      
      color icon_color = GetIconColor(resizing);                 //--- Get icon color
      uint argb_icon = ColorToARGB(icon_color, 255);             //--- Convert to ARGB
      
      canvasGraph.FontSet(icon_font, icon_size);                 //--- Set font
      canvasGraph.FontAngleSet(angle);                           //--- Set angle
      
      int icon_x = 0;                                            //--- Initialize x
      int icon_y = 0;                                            //--- Initialize y
      
      switch (active_mode)                                       //--- Switch for position
      {
         case BOTTOM:
            icon_x = MathMax(0, MathMin(hover_mouse_local_x - (icon_size / 2), currentWidth - icon_size)); //--- Set x
            icon_y = currentHeight - icon_size - 2;              //--- Set y
            break;
         case RIGHT:
            icon_y = MathMax(0, MathMin(hover_mouse_local_y - (icon_size / 2), currentHeight - icon_size)); //--- Set y
            icon_x = currentWidth - icon_size - 2;               //--- Set x
            break;
         case BOTTOM_RIGHT:
            icon_x = currentWidth - icon_size - 10;              //--- Set x
            icon_y = currentHeight - icon_size;                  //--- Set y
            break;
         default: break;
      }
      
      canvasGraph.TextOut(icon_x, icon_y, icon_symbol, argb_icon, TA_LEFT | TA_TOP); //--- Draw icon
      canvasGraph.FontAngleSet(0);                               //--- Reset angle
   }
   
   canvasGraph.Update();                                         //--- Update canvas
}

ここではUpdateGraphOnCanvas関数を実装し、canvasGraphオブジェクト上に価格グラフを描画します。最近のバーの終値をラインプロットとして表示し、塗りつぶし領域、ラベル、および任意のリサイズインジケータも含みます。まずEraseメソッドを使用し、Canvasをゼロで初期化します。UseBackgroundがtrueであり、かつbg_pixels_graphが現在のCanvas寸法と一致する場合、ループで高さと幅を走査し、PixelSetメソッドを用いてスケーリング済み背景配列から各ピクセルを設定します。次にGetBorderColorで取得した色を ColorToARGBでアルファ255のARGBに変換し、Lineメソッドを用いて外枠および内枠の枠線を上下左右に描画し、二重枠線効果を生成します。

closePricesというdouble配列をgraphBarsサイズにリサイズし、 CopyCloseを用いて現在のシンボルおよび時間足の終値をバー0から取得します。不完全な取得の場合はエラーメッセージを出力し処理を終了します。同様にCopyTimeを用いてtimeArrというdatetime配列へ時刻を取得し、失敗時はエラー処理をおこないます。次に最小値および最大値を算出するため、初期値を最初のcloseに設定しループで更新します。レンジを計算し、ゼロ除算を回避するために0の場合は_Pointをデフォルト値として使用します。

グラフ描画用のマージンとしてgraphLeftを2、graphRightをwidth - 3に設定し、有効幅と高さを計算します。bottom yは2 + heightとします。x_posおよびy_posの整数配列をgraphBarsサイズにリサイズし、ループで正規化座標を計算します。xはleft + 正規化比率×幅を四捨五入して求め、yは2 + (max - price) / range ×高さを反転して計算します。最近のデータが左に来るようにインデックスを反転させます。ライン色を青に設定しColorToARGBでARGBに変換した後、LineAAを用いてセグメントごとにアンチエイリアス付きのラインを描画します。

塗りつぶし処理では、反転後のmin xとmax xを取得し、右から左へ列ごとにループします。各xについて論理xを導出し、該当するセグメントを探索して存在しない場合はスキップします。補間係数tを計算し、y値を補間して整数化します。その後、上方向からbottom yまでループし、bottomからの距離に基づいてfade係数を算出し、alphaを255×fade×FogOpacityとして計算します。BlendFogがtrueの場合はPixelGetで既存ピクセルを取得し、BlendPixelsヘルパー関数でブレンドしてPixelSetします。falseの場合はそのままPixelSetで書き込みます。ピクセルブレンディングにはカスタムヘルパー関数を使用しており、そのコードスニペットは以下の通りです。

//+------------------------------------------------------------------+
//| Alpha blending function for two ARGB colors                      |
//+------------------------------------------------------------------+
uint BlendPixels(uint bg, uint fg)
{
   uchar bgA = (uchar)((bg >> 24) & 0xFF);                       //--- Get bg alpha
   uchar bgR = (uchar)((bg >> 16) & 0xFF);                       //--- Get bg red
   uchar bgG = (uchar)((bg >> 8) & 0xFF);                        //--- Get bg green
   uchar bgB = (uchar)(bg & 0xFF);                               //--- Get bg blue
   
   uchar fgA = (uchar)((fg >> 24) & 0xFF);                       //--- Get fg alpha
   uchar fgR = (uchar)((fg >> 16) & 0xFF);                       //--- Get fg red
   uchar fgG = (uchar)((fg >> 8) & 0xFF);                        //--- Get fg green
   uchar fgB = (uchar)(fg & 0xFF);                               //--- Get fg blue
   
   if (fgA == 0) return bg;                                      //--- Return bg if transparent
   if (fgA == 255) return fg;                                    //--- Return fg if opaque
   
   double alphaFg = fgA / 255.0;                                 //--- Compute fg alpha
   double alphaBg = 1.0 - alphaFg;                               //--- Compute bg alpha
   
   uchar outR = (uchar)(fgR * alphaFg + bgR * alphaBg);          //--- Blend red
   uchar outG = (uchar)(fgG * alphaFg + bgG * alphaBg);          //--- Blend green
   uchar outB = (uchar)(fgB * alphaFg + bgB * alphaBg);          //--- Blend blue
   uchar outA = (uchar)(fgA + bgA * alphaBg);                    //--- Blend alpha
   
   return (((uint)outA) << 24) | (((uint)outR) << 16) | (((uint)outG) << 8) | ((uint)outB); //--- Return blended
}

この関数では、ARGB関数と同様のアプローチを用い、ビット演算によって処理をおこないます。可読性向上のため、コメントも追加しています。続いてフォントをArial、サイズ12に設定し、テーマに応じたテキストカラー(ダークモードでは黒、ライトモードではグレー)をARGB形式へ変換します。その後、シンボル付きのタイトルPrice Graphを中央揃えで上部に描画します。最新および最古の時刻についてはTimeToStringを用いて日付および分単位でフォーマットし、左下には左揃えで、右下には右揃えで描画します。

resize_hoveredまたはresizingがtrueの場合、resize_modeまたはhover_modeからアクティブモードを取得し、該当しない場合は早期リターンします。アイコンフォントにはサイズ25のWingdings 3を設定します。続いて、モードに応じてコードと回転角度を決定します。下端または右端のモードでは文字コードを「2」または「1」、回転角度を0度とし、コーナーモードでは文字コードを「2」、回転角度を45度に設定します。最後に、CharToString関数を使用してシンボル文字列へ変換します。アイコンについては、スタイルに応じて適切なものを選択できます。今回はMQL5に標準的なカーソル変更機能がないため、代替として工夫してこの方法を採用しています。使用可能なフォントシンボルの例は以下の通りです。

記号フォント

次にアイコン色をGetIconColorから取得し、リサイズ状態を考慮してARGBへ変換します。フォントと角度をFontSetおよびFontAngleSetで設定し、モードに応じて位置を算出します。位置計算ではMathMaxおよびMathMinを用いて範囲をクランプし、ホバー時のローカル座標または固定オフセットを適用します。TextOutを左上揃えで描画し、最後に角度を0にリセットします。最終的にUpdateを呼び出してCanvasへ反映します。この関数を初期化イベントハンドラから呼び出すことで、以下のようなグラフCanvasの出力が得られます。

グラフCanvas

グラフCanvasの描画が完了したため、次は右側に配置する統計パネルを作成します。このパネルでは、背景が接する部分で2色を線形補間してブレンドし、シンプルながらも少し発展的な表現を導入します。またこれまでヘッダーやグラフCanvasで使用していた静的な枠線カラーではなく、選択された背景色に基づいて枠線を暗くする処理をおこないます。そのためにはいくつかのヘルパー関数が必要になります。

//+------------------------------------------------------------------+
//| Linear interpolation between two colors                          |
//+------------------------------------------------------------------+
color InterpolateColor(color start, color end, double factor)
{
   uchar r1 = (uchar)((start >> 16) & 0xFF);                     //--- Get start red
   uchar g1 = (uchar)((start >> 8) & 0xFF);                      //--- Get start green
   uchar b1 = (uchar)(start & 0xFF);                             //--- Get start blue
   
   uchar r2 = (uchar)((end >> 16) & 0xFF);                       //--- Get end red
   uchar g2 = (uchar)((end >> 8) & 0xFF);                        //--- Get end green
   uchar b2 = (uchar)(end & 0xFF);                               //--- Get end blue
   
   uchar r = (uchar)(r1 + factor * (r2 - r1));                   //--- Interpolate red
   uchar g = (uchar)(g1 + factor * (g2 - g1));                   //--- Interpolate green
   uchar b = (uchar)(b1 + factor * (b2 - b1));                   //--- Interpolate blue
   
   return (r << 16) | (g << 8) | b;                              //--- Return color
}

//+------------------------------------------------------------------+
//| Darken a color by a factor (0.0 to 1.0)                         |
//+------------------------------------------------------------------+
color DarkenColor(color colorValue, double factor)
{
   int blue = int((colorValue & 0xFF) * factor);                 //--- Darken blue
   int green = int(((colorValue >> 8) & 0xFF) * factor);         //--- Darken green
   int red = int(((colorValue >> 16) & 0xFF) * factor);          //--- Darken red
   return (color)(blue | (green << 8) | (red << 16));            //--- Return darkened
}

まず、InterpolateColor関数を実装します。この関数は0から1の係数に基づいて2つの色を線形補間し、グラデーションなどのエフェクト用に補間結果の色を返します。引数としてstartとendのカラー値、およびdouble型のfactorを受け取ります。startからは赤、緑、青の各成分を、それぞれ16ビット、8ビット、0ビットの右シフトと0xFFマスクを用いてucharとして抽出します。同様にendからも各成分を抽出します。各チャンネルはstart値に対して(endとの差分 × factor)を加算する形で補間し、その結果をucharにキャストします。最後に赤を16ビット左シフト、緑を8ビット左シフト、青をそのままOR演算で結合し、最終的なカラー値として返します。

次にDarkenColor関数を作成します。この関数は0から1の係数に基づいて色の明度を下げ、factorが1の場合は元の色を維持し、それより小さい場合は暗くなります。引数としてcolorValueとdouble型のfactorを受け取ります。各チャンネルはcolorValueからビットシフトと0xFFマスクを用いて取得し、それぞれにfactorを掛けて暗くします。青、緑、赤の順に処理し、結果をそれぞれOR演算で結合し、最終的なcolorとして返します。これらのヘルパー関数を用いることで、統計パネルの作成処理をより柔軟に構築できるようになります。

//+------------------------------------------------------------------+
//| Update the stats on the second Canvas                            |
//+------------------------------------------------------------------+
void UpdateStatsOnCanvas()
{
   canvasStats.Erase(0);                                         //--- Clear canvas
   
   int statsWidth = currentWidth / 2;                            //--- Compute width
   if (UseBackground && ArraySize(bg_pixels_stats) == statsWidth * currentHeight) //--- Check background
   {
      for (int y = 0; y < currentHeight; y++)                    //--- Loop rows
      {
         for (int x = 0; x < statsWidth; x++)                    //--- Loop columns
         {
            canvasStats.PixelSet(x, y, bg_pixels_stats[y * statsWidth + x]); //--- Set pixel
         }
      }
   }
   
   if (StatsBackgroundMode != NoColor)                           //--- Check mode
   {
      for (int y = 0; y < currentHeight; y++)                    //--- Loop rows
      {
         double factor = (double)y / (currentHeight - 1);        //--- Compute factor
         color currentColor = (StatsBackgroundMode == SingleColor) ? GetTopColor() : InterpolateColor(GetTopColor(), GetBottomColor(), factor); //--- Get color
         uchar alpha = (uchar)(255 * BackgroundOpacity);         //--- Compute alpha
         uint argbFill = ColorToARGB(currentColor, alpha);       //--- Convert fill
         
         for (int x = 0; x < statsWidth; x++)                    //--- Loop columns
         {
            uint currentPixel = canvasStats.PixelGet(x, y);      //--- Get pixel
            uint blendedPixel = BlendPixels(currentPixel, argbFill); //--- Blend
            canvasStats.PixelSet(x, y, blendedPixel);            //--- Set blended
         }
      }
   }
   
   if (StatsBackgroundMode != NoColor)                           //--- Check mode for borders
   {
      double reduction = BorderOpacityPercentReduction / 100.0;  //--- Compute reduction
      double opacity = MathMax(0.0, MathMin(1.0, BackgroundOpacity * (1.0 - reduction))); //--- Compute opacity
      uchar alpha = (uchar)(255 * opacity);                      //--- Set alpha
      double darkenReduction = BorderDarkenPercent / 100.0;      //--- Compute darken
      double darkenFactor = MathMax(0.0, MathMin(1.0, 1.0 - darkenReduction)); //--- Set factor
      
      for (int y = 0; y < currentHeight; y++)                    //--- Loop vertical
      {
         double factor = (StatsBackgroundMode == SingleColor) ? 0.0 : (double)y / (currentHeight - 1); //--- Get factor
         color baseColor = (StatsBackgroundMode == SingleColor) ? GetTopColor() : InterpolateColor(GetTopColor(), GetBottomColor(), factor); //--- Get base
         color darkColor = DarkenColor(baseColor, darkenFactor); //--- Darken color
         uint argb = ColorToARGB(darkColor, alpha);              //--- Convert to ARGB
         
         canvasStats.PixelSet(0, y, argb);                       //--- Set left outer
         canvasStats.PixelSet(1, y, argb);                       //--- Set left inner
         
         canvasStats.PixelSet(statsWidth - 1, y, argb);          //--- Set right outer
         canvasStats.PixelSet(statsWidth - 2, y, argb);          //--- Set right inner
      }
      
      double factorTop = 0.0;                                    //--- Set top factor
      color baseTop = GetTopColor();                             //--- Get top base
      color darkTop = DarkenColor(baseTop, darkenFactor);        //--- Darken top
      uint argbTop = ColorToARGB(darkTop, alpha);                //--- Convert top
      for (int x = 0; x < statsWidth; x++)                       //--- Loop top
      {
         canvasStats.PixelSet(x, 0, argbTop);                    //--- Set top outer
         canvasStats.PixelSet(x, 1, argbTop);                    //--- Set top inner
      }
      
      double factorBot = (StatsBackgroundMode == SingleColor) ? 0.0 : 1.0; //--- Set bottom factor
      color baseBot = (StatsBackgroundMode == SingleColor) ? GetTopColor() : GetBottomColor(); //--- Get bottom base
      color darkBot = DarkenColor(baseBot, darkenFactor);        //--- Darken bottom
      uint argbBot = ColorToARGB(darkBot, alpha);                //--- Convert bottom
      for (int x = 0; x < statsWidth; x++)                       //--- Loop bottom
      {
         canvasStats.PixelSet(x, currentHeight - 1, argbBot);    //--- Set bottom outer
         canvasStats.PixelSet(x, currentHeight - 2, argbBot);    //--- Set bottom inner
      }
   }
   else                                                          //--- Handle no color
   {
      uint argbBorder = ColorToARGB(GetBorderColor(), 255);      //--- Convert border
      canvasStats.Line(0, 0, statsWidth - 1, 0, argbBorder);     //--- Draw top outer
      canvasStats.Line(statsWidth - 1, 0, statsWidth - 1, currentHeight - 1, argbBorder); //--- Draw right outer
      canvasStats.Line(statsWidth - 1, currentHeight - 1, 0, currentHeight - 1, argbBorder); //--- Draw bottom outer
      canvasStats.Line(0, currentHeight - 1, 0, 0, argbBorder);  //--- Draw left outer
      canvasStats.Line(1, 1, statsWidth - 2, 1, argbBorder);     //--- Draw top inner
      canvasStats.Line(statsWidth - 2, 1, statsWidth - 2, currentHeight - 2, argbBorder); //--- Draw right inner
      canvasStats.Line(statsWidth - 2, currentHeight - 2, 1, currentHeight - 2, argbBorder); //--- Draw bottom inner
      canvasStats.Line(1, currentHeight - 2, 1, 1, argbBorder);  //--- Draw left inner
   }
   
   color labelColor = GetStatsLabelColor();                      //--- Get label color
   color valueColor = GetStatsValueColor();                      //--- Get value color
   color headerColor = GetStatsHeaderColor();                    //--- Get header color
   
   int yPos = 20;                                                //--- Set initial y
   canvasStats.FontSet("Arial Bold", StatsHeaderFontSize);       //--- Set header font
   uint argbHeader = ColorToARGB(headerColor, 255);              //--- Convert header
   canvasStats.TextOut(statsWidth / 2, yPos, "Account Stats", argbHeader, TA_CENTER); //--- Draw account header
   yPos += 30;                                                   //--- Increment y
   
   canvasStats.FontSet("Arial Bold", StatsFontSize);             //--- Set font
   uint argbLabel = ColorToARGB(labelColor, 255);                //--- Convert label
   uint argbValue = ColorToARGB(valueColor, 255);                //--- Convert value
   
   canvasStats.TextOut(10, yPos, "Name:", argbLabel, TA_LEFT);   //--- Draw name label
   canvasStats.TextOut(statsWidth - 10, yPos, AccountInfoString(ACCOUNT_NAME), argbValue, TA_RIGHT); //--- Draw name value
   yPos += 20;                                                   //--- Increment y
   
   canvasStats.TextOut(10, yPos, "Balance:", argbLabel, TA_LEFT); //--- Draw balance label
   canvasStats.TextOut(statsWidth - 10, yPos, DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2), argbValue, TA_RIGHT); //--- Draw balance value
   yPos += 20;                                                   //--- Increment y
   
   canvasStats.TextOut(10, yPos, "Equity:", argbLabel, TA_LEFT); //--- Draw equity label
   canvasStats.TextOut(statsWidth - 10, yPos, DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2), argbValue, TA_RIGHT); //--- Draw equity value
   yPos += 30;                                                   //--- Increment y
   
   canvasStats.FontSet("Arial Bold", StatsHeaderFontSize);       //--- Set header font
   canvasStats.TextOut(statsWidth / 2, yPos, "Current Bar Stats", argbHeader, TA_CENTER); //--- Draw bar header
   yPos += 30;                                                   //--- Increment y
   
   canvasStats.FontSet("Arial Bold", StatsFontSize);             //--- Set font
   
   double barOpen = iOpen(_Symbol, _Period, 0);                  //--- Get open
   double barHigh = iHigh(_Symbol, _Period, 0);                  //--- Get high
   double barLow = iLow(_Symbol, _Period, 0);                    //--- Get low
   double barClose = iClose(_Symbol, _Period, 0);                //--- Get close
   
   canvasStats.TextOut(10, yPos, "Open:", argbLabel, TA_LEFT);   //--- Draw open label
   canvasStats.TextOut(statsWidth - 10, yPos, DoubleToString(barOpen, _Digits), argbValue, TA_RIGHT); //--- Draw open value
   yPos += 20;                                                   //--- Increment y
   
   canvasStats.TextOut(10, yPos, "High:", argbLabel, TA_LEFT);   //--- Draw high label
   canvasStats.TextOut(statsWidth - 10, yPos, DoubleToString(barHigh, _Digits), argbValue, TA_RIGHT); //--- Draw high value
   yPos += 20;                                                   //--- Increment y
   
   canvasStats.TextOut(10, yPos, "Low:", argbLabel, TA_LEFT);    //--- Draw low label
   canvasStats.TextOut(statsWidth - 10, yPos, DoubleToString(barLow, _Digits), argbValue, TA_RIGHT); //--- Draw low value
   yPos += 20;                                                   //--- Increment y
   
   canvasStats.TextOut(10, yPos, "Close:", argbLabel, TA_LEFT);  //--- Draw close label
   canvasStats.TextOut(statsWidth - 10, yPos, DoubleToString(barClose, _Digits), argbValue, TA_RIGHT); //--- Draw close value
   
   canvasStats.Update();                                         //--- Update canvas
}

ここではUpdateStatsOnCanvas関数を実装し、canvasStatsオブジェクト上に統計パネルを描画します。このパネルでは、アカウント情報および現在バーの詳細を、テーマ付き背景、塗りつぶし、枠線、テキストを用いて表示します。まずEraseメソッドを使用し、Canvasをクリアします。UseBackgroundがtrueであり、かつbg_pixels_statsが現在の寸法(グラフ幅の半分×高さ)と一致する場合、行と列をループし、PixelSetメソッドを用いてスケーリング済み背景配列から各ピクセルを設定します。

StatsBackgroundModeがNoColorでない場合、まず高さ方向にループし、各行の垂直ファクターを計算します。単色モードの場合はGetTopColorを使用し、グラデーションモードの場合はInterpolateColorを用いてトップカラーとボトムカラーの間を補間して行ごとの色を決定します。次にBackgroundOpacityに255を掛けてアルファ値を算出しARGBへ変換します。その後、各xについて現在のピクセルをPixelGetで取得し、BlendPixels関数を用いて塗りつぶし色とブレンドし、結果をPixelSetで設定します。

枠線を塗りつぶしモードで描画する場合は、まずBorderOpacityPercentReductionを100.0で割って不透明度の減少率を求めます。次に、その値をBackgroundOpacityに乗算し、0.0~1.0の範囲にクランプしたうえで、255を掛けてアルファ値を算出します。また、暗色化係数については、BorderDarkenPercentを100.0で割った値を1.0から減算し、その結果を有効範囲内にクランプして求めます。行ごとにループし、垂直ファクター(単色の場合は0.0、それ以外は正規化値)を計算し、ベースカラーをトップまたは補間結果から取得します。その後DarkenColor関数で色を暗くし、ARGBに変換した上で左外側、左内側、右外側、右内側のピクセルを設定します。最上行についてはファクター0.0を使用し、トップカラーをベースとして暗化処理をおこない、ARGB変換後に横方向の外枠と内枠を描画します。同様に最下行ではファクター1.0(または単色時は0.0)を使用し、ボトム側の境界を描画します。塗りつぶししない場合はGetBorderColorをARGB(アルファ255)に変換し、Lineメソッドを用いて外枠および内枠の上下左右のラインを描画します。

その後、ラベル、値、ヘッダー用のテーマカラーを各getter関数から取得します。初期y位置を20に設定し、フォントをStatsHeaderFontSizeのArial Boldに設定してヘッダーカラーをARGBへ変換し、TextOutを用いて「Account Stats」を中央揃えで描画し、その後yを30増加させます。

次にフォントをStatsFontSizeサイズのArial Boldへ変更し、ラベルと値のカラーをARGBへ変換します。Nameについては左x=10にラベルを描画し、右端(width - 10)にAccountInfoStringからACCOUNT_NAMEを取得して右揃えで描画します。その後yを20増加させます。同様にBalanceではAccountInfoDoubleでACCOUNT_BALANCEを取得し、小数点2桁で表示します。EquityについてもAccountInfoDoubleでACCOUNT_EQUITYを取得して表示します。それぞれ描画後にyを20ずつ増加させます。次にヘッダーフォントへ切り替え、「Current Bar Stats」を中央揃えで描画し、yを30増加させます。その後フォントを元に戻し、現在バーのOHLCをiOpen、iHigh、iLow、iClose関数を用いて取得します(銘柄、時間足、バー0)。Openについては「Open:」ラベルと値を表示し、値は_Digits桁でフォーマットして右揃えで描画します。同様にHigh、Low、Closeについても同じ処理を繰り返します。それぞれ描画後にyを20ずつ増加させます。最後にUpdateメソッドを呼び出してCanvasを更新し、統計情報を画面に反映します。コンパイルすると、次の結果が得られます。

統計パネル

画像から確認できるように、カラー補間を用いた統計パネルが正しく構築されています。補間を使用したくない場合は、ハードバウンダリ方式での色分けとして実装することも可能です。その場合は、単純に数式による補間処理を省略すれば問題ありません。

//+------------------------------------------------------------------+
//| Color selection WITHOUT interpolation (hard switch only)         |
//+------------------------------------------------------------------+
color InterpolateColor(color start, color end, double factor)
{
   // Clamp factor just to be safe
   if(factor <= 0.0)
      return start;

   if(factor >= 1.0)
      return end;

   // HARD boundary — no mixing at all
   return (factor < 0.5 ? start : end);
}

この方法を用いると、以下の結果が得られます。

ハードバウンダリーカラーミキシング結果

境界部分は線形にブレンドされていないことが確認できます。そのため、どのアプローチを採用するかは最終的にユーザーのスタイル次第となります。以上でダッシュボードのレンダリングは完了し、選択したデフォルトのダークテーマモードに対応した表示が構築されました。チャートのインタラクションを有効化するためには、初期化処理の中でマウスムーブメントを有効にする必要があります。その結果、最終的な初期化イベントハンドラは以下のようになります。

//+------------------------------------------------------------------+
//| Initialize expert                                                |
//+------------------------------------------------------------------+
int OnInit()

   //--- Existing init logic
   
   DrawHeaderOnCanvas();                                         //--- Draw header
   UpdateGraphOnCanvas();                                        //--- Update graph
   if (EnableStatsPanel) UpdateStatsOnCanvas();                  //--- Update stats
   
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);             //--- Enable mouse events
   ChartRedraw();                                                //--- Redraw chart
   return(INIT_SUCCEEDED);                                       //--- Return success
}

ChartSetInteger関数を使って、マウス移動の検知をtrueに設定するだけで構いません。これにより、チャート上でのマウスムーブメントが有効になります。これで設定は完了しており、次にダッシュボードオブジェクトとのインタラクションをおこなうために、マウス状態を追跡し、それに応じて状態を変更するためのヘルパー関数を作成していきます。

//+------------------------------------------------------------------+
//| Check if mouse is over header (excluding buttons)                |
//+------------------------------------------------------------------+
bool IsMouseOverHeader(int mouse_x, int mouse_y)
{
   int header_x = currentCanvasX;                                //--- Get header x
   int header_y = currentCanvasY;                                //--- Get header y
   int header_w = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute width
   int header_h = header_height;                                 //--- Get height
   
   if (mouse_x < header_x || mouse_x > header_x + header_w || mouse_y < header_y || mouse_y > header_y + header_h) return false; //--- Check outside
   
   int theme_left = header_x + header_w + theme_x_offset - button_size / 2; //--- Compute theme left
   int theme_right = theme_left + button_size;                   //--- Compute theme right
   int theme_top = header_y;                                     //--- Set theme top
   int theme_bottom = theme_top + header_h;                      //--- Compute theme bottom
   if (mouse_x >= theme_left && mouse_x <= theme_right && mouse_y >= theme_top && mouse_y <= theme_bottom) return false; //--- Check in theme
   
   int min_left = header_x + header_w + minimize_x_offset - button_size / 2; //--- Compute minimize left
   int min_right = min_left + button_size;                       //--- Compute minimize right
   int min_top = header_y;                                       //--- Set minimize top
   int min_bottom = min_top + header_h;                          //--- Compute minimize bottom
   if (mouse_x >= min_left && mouse_x <= min_right && mouse_y >= min_top && mouse_y <= min_bottom) return false; //--- Check in minimize
   
   int close_left = header_x + header_w + close_x_offset - button_size / 2; //--- Compute close left
   int close_right = close_left + button_size;                   //--- Compute close right
   int close_top = header_y;                                     //--- Set close top
   int close_bottom = close_top + header_h;                      //--- Compute close bottom
   if (mouse_x >= close_left && mouse_x <= close_right && mouse_y >= close_top && mouse_y <= close_bottom) return false; //--- Check in close
   
   return true;                                                  //--- Return in header
}

//+------------------------------------------------------------------+
//| Check if mouse over theme button                                 |
//+------------------------------------------------------------------+
bool IsMouseOverTheme(int mouse_x, int mouse_y)
{
   int header_w = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute width
   int theme_left = currentCanvasX + header_w + theme_x_offset - button_size / 2; //--- Compute left
   int theme_right = theme_left + button_size;                   //--- Compute right
   int theme_top = currentCanvasY;                               //--- Set top
   int theme_bottom = theme_top + header_height;                 //--- Compute bottom
   return (mouse_x >= theme_left && mouse_x <= theme_right && mouse_y >= theme_top && mouse_y <= theme_bottom); //--- Check in theme
}

//+------------------------------------------------------------------+
//| Check if mouse over minimize button                              |
//+------------------------------------------------------------------+
bool IsMouseOverMinimize(int mouse_x, int mouse_y)
{
   int header_w = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute width
   int min_left = currentCanvasX + header_w + minimize_x_offset - button_size / 2; //--- Compute left
   int min_right = min_left + button_size;                       //--- Compute right
   int min_top = currentCanvasY;                                 //--- Set top
   int min_bottom = min_top + header_height;                     //--- Compute bottom
   return (mouse_x >= min_left && mouse_x <= min_right && mouse_y >= min_top && mouse_y <= min_bottom); //--- Check in minimize
}

//+------------------------------------------------------------------+
//| Check if mouse over close button                                 |
//+------------------------------------------------------------------+
bool IsMouseOverClose(int mouse_x, int mouse_y)
{
   int header_w = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute width
   int close_left = currentCanvasX + header_w + close_x_offset - button_size / 2; //--- Compute left
   int close_right = close_left + button_size;                   //--- Compute right
   int close_top = currentCanvasY;                               //--- Set top
   int close_bottom = close_top + header_height;                 //--- Compute bottom
   return (mouse_x >= close_left && mouse_x <= close_right && mouse_y >= close_top && mouse_y <= close_bottom); //--- Check in close
}

//+------------------------------------------------------------------+
//| Check if mouse over resize borders                               |
//+------------------------------------------------------------------+
bool IsMouseOverResize(int mx, int my, ENUM_RESIZE_MODE &rmode)
{
   if (panels_minimized) return false;                           //--- Check if minimized
   int graph_x = currentCanvasX;                                 //--- Get graph x
   int graph_y = currentCanvasY + header_height + gap_y;         //--- Get graph y
   int graph_right = graph_x + currentWidth;                     //--- Compute right
   int graph_bottom = graph_y + currentHeight;                   //--- Compute bottom
   bool over_right = (mx >= graph_right - resize_thickness && mx <= graph_right + resize_thickness) && (my >= graph_y && my <= graph_bottom); //--- Check right
   bool over_bottom = (my >= graph_bottom - resize_thickness && my <= graph_bottom + resize_thickness) && (mx >= graph_x && mx <= graph_right); //--- Check bottom
   if (over_bottom && over_right)                                //--- Check corner
   {
      rmode = BOTTOM_RIGHT;                                      //--- Set bottom-right
      return true;                                               //--- Return true
   }
   else if (over_bottom)                                         //--- Check bottom only
   {
      rmode = BOTTOM;                                            //--- Set bottom
      return true;                                               //--- Return true
   }
   else if (over_right)                                          //--- Check right only
   {
      rmode = RIGHT;                                             //--- Set right
      return true;                                               //--- Return true
   }
   return false;                                                 //--- Return false
}

//+------------------------------------------------------------------+
//| Toggle theme                                                     |
//+------------------------------------------------------------------+
void ToggleTheme()
{
   is_dark_theme = !is_dark_theme;                               //--- Switch theme
   Print("Switched to ", (is_dark_theme ? "Dark" : "Light"), " theme"); //--- Print switch
   DrawHeaderOnCanvas();                                         //--- Redraw header
   UpdateGraphOnCanvas();                                        //--- Update graph
   if (EnableStatsPanel) UpdateStatsOnCanvas();                  //--- Update stats
   ChartRedraw();                                                //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Toggle minimize state                                            |
//+------------------------------------------------------------------+
void ToggleMinimize()
{
   panels_minimized = !panels_minimized;                         //--- Toggle minimized
   if (panels_minimized)                                         //--- Handle minimize
   {
      canvasGraph.Destroy();                                     //--- Destroy graph
      graphCreated = false;                                      //--- Reset graph flag
      if (EnableStatsPanel)                                      //--- Check stats
      {
         canvasStats.Destroy();                                  //--- Destroy stats
         statsCreated = false;                                   //--- Reset stats flag
      }
   }
   else                                                          //--- Handle maximize
   {
      if (!canvasGraph.CreateBitmapLabel(0, 0, canvasGraphName, currentCanvasX, currentCanvasY + header_height + gap_y, currentWidth, currentHeight, COLOR_FORMAT_ARGB_NORMALIZE)) //--- Recreate graph
      {
         Print("Failed to recreate Graph Canvas");               //--- Print error
      }
      graphCreated = true;                                       //--- Set graph flag
      UpdateGraphOnCanvas();                                     //--- Update graph
      if (EnableStatsPanel)                                      //--- Check stats
      {
         int statsX = currentCanvasX + currentWidth + PanelGap;  //--- Compute stats X
         if (!canvasStats.CreateBitmapLabel(0, 0, canvasStatsName, statsX, currentCanvasY + header_height + gap_y, currentWidth / 2, currentHeight, COLOR_FORMAT_ARGB_NORMALIZE)) //--- Recreate stats
         {
            Print("Failed to recreate Stats Canvas");            //--- Print error
         }
         statsCreated = true;                                    //--- Set stats flag
         UpdateStatsOnCanvas();                                  //--- Update stats
      }
   }
   
   int new_header_width = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute new width
   canvasHeader.Resize(new_header_width, header_height);         //--- Resize header
   ObjectSetInteger(0, canvasHeaderName, OBJPROP_XSIZE, new_header_width); //--- Update header width
   ObjectSetInteger(0, canvasHeaderName, OBJPROP_YSIZE, header_height); //--- Update header height
   DrawHeaderOnCanvas();                                         //--- Redraw header
   canvasHeader.Update();                                        //--- Update header
   ChartRedraw();                                                //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Close the dashboard                                              |
//+------------------------------------------------------------------+
void CloseDashboard()
{
   canvasHeader.Destroy();                                       //--- Destroy header
   canvasGraph.Destroy();                                        //--- Destroy graph
   if (EnableStatsPanel) canvasStats.Destroy();                  //--- Destroy stats
   ChartRedraw();                                                //--- Redraw chart
}

ここでは、特定の領域に対するマウス位置を判定するための複数のホバー検出関数を実装します。これにはヘッダー領域(ボタン領域を除外した部分)や、テーマ、最小化、クローズ各ボタン上の判定が含まれ、現在の位置とサイズ情報を用いて状態更新用のboolean値を返します。また、グラフパネルのリサイズ枠線に対する判定も追加し、ボーダーの太さや位置に基づいてbottom、right、cornerといったモードを判別し、ホバー状態またはリサイズ状態の列挙値を更新します。

さらにテーマ切り替え用の関数も作成します。フラグを反転させ、新しいモードをログ出力し、全Canvasを再描画します。同様に最小化処理では状態を切り替え、必要に応じてグラフおよび統計Canvasを破棄および再生成し、ヘッダーサイズを調整したうえで再描画と更新をおこないます。最後にクローズ処理では、すべてのCanvasを破棄しチャートを再描画します。IsMouseOverHeader関数では、ヘッダー領域上にマウスがあるかを判定します。ただしボタン領域は除外します。ヘッダーの位置はcurrentCanvasXおよびcurrentCanvasYから取得し、幅はグラフが最小化されていない場合に統計パネルとギャップを含めて計算し、高さはheader_heightから取得します。範囲外であればfalseを返します。

その後、theme、minimize、close各ボタンの領域をbutton_sizeとオフセット値から計算し、いずれかに該当すればfalseを返し、それ以外の場合にヘッダーホバーとしてtrueを返します。その他の関数については、すでに前回のセクションで類似のアプローチを使用しているため詳細な説明は省略しますが、必要なコメントは追加しています。次にこれらの関数をチャートイベントハンドラ内で使用します。

//+------------------------------------------------------------------+
//| Handle chart event                                               |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   if (id == CHARTEVENT_CHART_CHANGE)                            //--- Check change event
   {
      DrawHeaderOnCanvas();                                      //--- Redraw header
      UpdateGraphOnCanvas();                                     //--- Update graph
      if (EnableStatsPanel) UpdateStatsOnCanvas();               //--- Update stats
      ChartRedraw();                                             //--- Redraw chart
   }
   else if (id == CHARTEVENT_MOUSE_MOVE)                         //--- Handle mouse move
   {
      int mouse_x = (int)lparam;                                 //--- Get mouse x
      int mouse_y = (int)dparam;                                 //--- Get mouse y
      int mouse_state = (int)sparam;                             //--- Get mouse state
      
      bool prev_header_hovered = header_hovered;                 //--- Store previous header
      bool prev_min_hovered = minimize_hovered;                  //--- Store previous minimize
      bool prev_close_hovered = close_hovered;                   //--- Store previous close
      bool prev_theme_hovered = theme_hovered;                   //--- Store previous theme
      bool prev_resize_hovered = resize_hovered;                 //--- Store previous resize
      
      header_hovered = IsMouseOverHeader(mouse_x, mouse_y);      //--- Check header hover
      theme_hovered = IsMouseOverTheme(mouse_x, mouse_y);        //--- Check theme hover
      minimize_hovered = IsMouseOverMinimize(mouse_x, mouse_y); //--- Check minimize hover
      close_hovered = IsMouseOverClose(mouse_x, mouse_y);        //--- Check close hover
      resize_hovered = IsMouseOverResize(mouse_x, mouse_y, hover_mode); //--- Check resize hover
      
      if (resize_hovered || resizing)                            //--- Check resize state
      {
         hover_mouse_local_x = mouse_x - currentCanvasX;         //--- Set local x
         hover_mouse_local_y = mouse_y - (currentCanvasY + header_height + gap_y); //--- Set local y
      }
      
      bool hover_changed = (prev_header_hovered != header_hovered || prev_min_hovered != minimize_hovered || 
                            prev_close_hovered != close_hovered || prev_theme_hovered != theme_hovered ||
                            prev_resize_hovered != resize_hovered); //--- Check change
      
      if (hover_changed)                                         //--- If changed
      {
         DrawHeaderOnCanvas();                                   //--- Redraw header
         UpdateGraphOnCanvas();                                  //--- Update graph
         ChartRedraw();                                          //--- Redraw chart
      }
      else if ((resize_hovered || resizing) && (mouse_x != last_mouse_x || mouse_y != last_mouse_y)) //--- Check position change
      {
         UpdateGraphOnCanvas();                                  //--- Update graph
         ChartRedraw();                                          //--- Redraw chart
      }
      
      string header_tooltip = "";                                //--- Initialize tooltip
      if (theme_hovered) header_tooltip = "Toggle Theme (Dark/Light)"; //--- Set theme tooltip
      else if (minimize_hovered) header_tooltip = panels_minimized ? "Maximize Panels" : "Minimize Panels"; //--- Set minimize tooltip
      else if (close_hovered) header_tooltip = "Close Dashboard"; //--- Set close tooltip
      ObjectSetString(0, canvasHeaderName, OBJPROP_TOOLTIP, header_tooltip); //--- Set header tooltip
      
      string resize_tooltip = "";                                //--- Initialize resize tooltip
      if (resize_hovered || resizing)                            //--- Check resize
      {
         ENUM_RESIZE_MODE active_mode = resizing ? resize_mode : hover_mode; //--- Get mode
         switch (active_mode)                                    //--- Switch mode
         {
            case BOTTOM: resize_tooltip = "Resize Bottom"; break; //--- Set bottom
            case RIGHT: resize_tooltip = "Resize Right"; break;  //--- Set right
            case BOTTOM_RIGHT: resize_tooltip = "Resize Bottom-Right"; break; //--- Set corner
            default: break;
         }
      }
      ObjectSetString(0, canvasGraphName, OBJPROP_TOOLTIP, resize_tooltip); //--- Set graph tooltip
      
      if (mouse_state == 1 && prev_mouse_state == 0)             //--- Check mouse down
      {
         if (header_hovered)                                     //--- Check header
         {
            panel_dragging = true;                               //--- Start drag
            panel_drag_x = mouse_x;                              //--- Set drag x
            panel_drag_y = mouse_y;                              //--- Set drag y
            panel_start_x = currentCanvasX;                      //--- Set start x
            panel_start_y = currentCanvasY;                      //--- Set start y
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false);       //--- Disable scroll
            DrawHeaderOnCanvas();                                //--- Show drag color
            ChartRedraw();                                       //--- Redraw chart
         }
         else if (theme_hovered)                                 //--- Check theme
         {
            ToggleTheme();                                       //--- Toggle theme
         }
         else if (minimize_hovered)                              //--- Check minimize
         {
            ToggleMinimize();                                    //--- Toggle minimize
         }
         else if (close_hovered)                                 //--- Check close
         {
            CloseDashboard();                                    //--- Close dashboard
         }
         else                                                    //--- Handle resize
         {
            ENUM_RESIZE_MODE temp_mode = NONE;                   //--- Initialize temp
            if (!panel_dragging && !resizing && IsMouseOverResize(mouse_x, mouse_y, temp_mode)) //--- Check resize
            {
               resizing = true;                                  //--- Start resizing
               resize_mode = temp_mode;                          //--- Set mode
               resize_start_x = mouse_x;                         //--- Set start x
               resize_start_y = mouse_y;                         //--- Set start y
               start_width = currentWidth;                       //--- Set start width
               start_height = currentHeight;                     //--- Set start height
               ChartSetInteger(0, CHART_MOUSE_SCROLL, false);    //--- Disable scroll
               UpdateGraphOnCanvas();                            //--- Show icon
               ChartRedraw();                                    //--- Redraw chart
            }
         }
      }
      else if (panel_dragging && mouse_state == 1)               //--- Handle dragging
      {
         int dx = mouse_x - panel_drag_x;                        //--- Compute dx
         int dy = mouse_y - panel_drag_y;                        //--- Compute dy
         int new_x = panel_start_x + dx;                         //--- Compute new x
         int new_y = panel_start_y + dy;                         //--- Compute new y
         
         int chart_w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get chart width
         int chart_h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Get chart height
         int full_w = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute full width
         int full_h = header_height + gap_y + (panels_minimized ? 0 : currentHeight); //--- Compute full height
         new_x = MathMax(0, MathMin(chart_w - full_w, new_x));   //--- Clamp x
         new_y = MathMax(0, MathMin(chart_h - full_h, new_y));   //--- Clamp y
         
         currentCanvasX = new_x;                                 //--- Update x
         currentCanvasY = new_y;                                 //--- Update y
         
         ObjectSetInteger(0, canvasHeaderName, OBJPROP_XDISTANCE, new_x); //--- Update header x
         ObjectSetInteger(0, canvasHeaderName, OBJPROP_YDISTANCE, new_y); //--- Update header y
         if (!panels_minimized)                                  //--- Check if shown
         {
            ObjectSetInteger(0, canvasGraphName, OBJPROP_XDISTANCE, new_x); //--- Update graph x
            ObjectSetInteger(0, canvasGraphName, OBJPROP_YDISTANCE, new_y + header_height + gap_y); //--- Update graph y
            if (EnableStatsPanel)                                //--- Check stats
            {
               int statsX = new_x + currentWidth + PanelGap;     //--- Compute stats x
               ObjectSetInteger(0, canvasStatsName, OBJPROP_XDISTANCE, statsX); //--- Update stats x
               ObjectSetInteger(0, canvasStatsName, OBJPROP_YDISTANCE, new_y + header_height + gap_y); //--- Update stats y
            }
         }
         ChartRedraw();                                          //--- Redraw chart
      }
      else if (resizing && mouse_state == 1)                     //--- Handle resizing
      {
         int dx = mouse_x - resize_start_x;                      //--- Compute dx
         int dy = mouse_y - resize_start_y;                      //--- Compute dy
         int new_width = currentWidth;                           //--- Initialize new width
         int new_height = currentHeight;                         //--- Initialize new height
         if (resize_mode == RIGHT || resize_mode == BOTTOM_RIGHT) //--- Check right
         {
            new_width = MathMax(min_width, start_width + dx);    //--- Update width
         }
         if (resize_mode == BOTTOM || resize_mode == BOTTOM_RIGHT) //--- Check bottom
         {
            new_height = MathMax(min_height, start_height + dy); //--- Update height
         }
         
         if (new_width != currentWidth || new_height != currentHeight) //--- Check change
         {
            int chart_w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get chart width
            int chart_h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Get chart height
            int avail_w = chart_w - currentCanvasX;              //--- Compute available width
            int avail_h = chart_h - (currentCanvasY + header_height + gap_y); //--- Compute available height
            new_height = MathMin(new_height, avail_h);           //--- Clamp height
            if (EnableStatsPanel)                                //--- Check stats
            {
               double max_w_d = (avail_w - PanelGap) / 1.5;      //--- Compute max width
               int max_w = (int)MathFloor(max_w_d);              //--- Floor max
               new_width = MathMin(new_width, max_w);            //--- Clamp width
            }
            else                                                 //--- No stats
            {
               new_width = MathMin(new_width, avail_w);          //--- Clamp width
            }
            
            currentWidth = new_width;                            //--- Update width
            currentHeight = new_height;                          //--- Update height
            
            if (UseBackground && ArraySize(original_bg_pixels) > 0) //--- Check background
            {
               ArrayCopy(bg_pixels_graph, original_bg_pixels);   //--- Copy graph
               ScaleImage(bg_pixels_graph, (int)orig_w, (int)orig_h, currentWidth, currentHeight); //--- Scale graph
               if (EnableStatsPanel)                             //--- Check stats
               {
                  ArrayCopy(bg_pixels_stats, original_bg_pixels); //--- Copy stats
                  ScaleImage(bg_pixels_stats, (int)orig_w, (int)orig_h, currentWidth / 2, currentHeight); //--- Scale stats
               }
            }
            
            canvasGraph.Resize(currentWidth, currentHeight);     //--- Resize graph
            ObjectSetInteger(0, canvasGraphName, OBJPROP_XSIZE, currentWidth); //--- Update graph width
            ObjectSetInteger(0, canvasGraphName, OBJPROP_YSIZE, currentHeight); //--- Update graph height
            
            if (EnableStatsPanel)                                //--- Check stats
            {
               int stats_width = currentWidth / 2;               //--- Compute stats width
               canvasStats.Resize(stats_width, currentHeight);   //--- Resize stats
               ObjectSetInteger(0, canvasStatsName, OBJPROP_XSIZE, stats_width); //--- Update stats width
               ObjectSetInteger(0, canvasStatsName, OBJPROP_YSIZE, currentHeight); //--- Update stats height
               int stats_x = currentCanvasX + currentWidth + PanelGap; //--- Compute stats x
               ObjectSetInteger(0, canvasStatsName, OBJPROP_XDISTANCE, stats_x); //--- Update stats x
            }
            
            canvasHeader.Resize(currentWidth + (EnableStatsPanel ? PanelGap + currentWidth / 2 : 0), header_height); //--- Resize header
            ObjectSetInteger(0, canvasHeaderName, OBJPROP_XSIZE, currentWidth + (EnableStatsPanel ? PanelGap + currentWidth / 2 : 0)); //--- Update header width
            ObjectSetInteger(0, canvasHeaderName, OBJPROP_YSIZE, header_height); //--- Update header height
            
            DrawHeaderOnCanvas();                                //--- Redraw header
            UpdateGraphOnCanvas();                               //--- Update graph
            if (EnableStatsPanel) UpdateStatsOnCanvas();         //--- Update stats
            ChartRedraw();                                       //--- Redraw chart
         }
      }
      else if (mouse_state == 0 && prev_mouse_state == 1)        //--- Check mouse up
      {
         if (panel_dragging)                                     //--- Check dragging
         {
            panel_dragging = false;                              //--- Stop drag
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true);        //--- Enable scroll
            DrawHeaderOnCanvas();                                //--- Reset color
            ChartRedraw();                                       //--- Redraw chart
         }
         if (resizing)                                           //--- Check resizing
         {
            resizing = false;                                    //--- Stop resize
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true);        //--- Enable scroll
            UpdateGraphOnCanvas();                               //--- Remove icon
            ChartRedraw();                                       //--- Redraw chart
         }
      }
      
      last_mouse_x = mouse_x;                                    //--- Update last x
      last_mouse_y = mouse_y;                                    //--- Update last y
      prev_mouse_state = mouse_state;                            //--- Update state
   }
}

OnChartEventイベントハンドラでは、idがCHARTEVENT_CHART_CHANGEの場合、DrawHeaderOnCanvasを呼び出してヘッダーを再描画し、UpdateGraphOnCanvasでグラフを更新し、EnableStatsPanelがtrueの場合はUpdateStatsOnCanvasも実行し、最後にChartRedrawで表示を更新します。CHARTEVENT_MOUSE_MOVEの場合は、lparamをマウスのx座標、dparamをy座標、sparamをステートとして整数にキャストします。前回のホバー状態をローカルに保持した上で、IsMouseOverHeader、IsMouseOverTheme、IsMouseOverMinimize、IsMouseOverClose、IsMouseOverResizeを用いて各ホバー状態を更新し、hover_modeへの参照も渡します。リサイズホバーまたはresizingがtrueの場合は、hover_mouse_local_xおよびhover_mouse_local_yをグラフ位置基準の相対座標として設定します。ホバー状態に変化がある場合は、前後を比較して判定し、変更があればヘッダーとグラフを再描画してChartRedrawを実行します。変化がない場合でも、リサイズ状態かつマウス位置がlast_mouse_xおよびlast_mouse_yと異なる場合はグラフ更新と再描画をおこないます。

ヘッダーツールチップ文字列は初期化され、theme_hoveredの場合はToggle Theme (Dark/Light)、minimize_hoveredの場合はpanels_minimizedに応じて最大化および最小化メッセージ、close_hoveredの場合はClose Dashboardが設定され、ObjectSetStringでcanvasHeaderNameのOBJPROP_TOOLTIPに適用されます。同様にリサイズ用ツールチップでは、resize_modeまたはhover_modeからアクティブモードを取得し、下、右、右下に応じた文字列を生成し、canvasGraphNameへ設定します。

マウス状態が1(押下)でprev_mouse_stateが0の場合、クリックイベントとして処理します。header_hoveredがtrueの場合はpanel_draggingをtrueにし、ドラッグ開始座標を保存し、ChartSetIntegerでCHART_MOUSE_SCROLLをfalseに設定してスクロールを無効化し、ヘッダーとグラフを再描画します。theme_hoveredの場合はToggleTheme、minimize_hoveredの場合はToggleMinimize、close_hoveredの場合はCloseDashboardを呼び出します。それ以外でドラッグやリサイズ中でなく、IsMouseOverResizeがtrueの場合はリサイズ開始としてresizingをtrueにし、resize_modeを設定し、開始位置とサイズを保存し、スクロールを無効化してグラフを更新して再描画します。

panel_draggingがtrueであり状態が1の場合はドラッグ中処理をおこないます。マウスの差分から新しいxおよびyを計算し、ChartGetIntegerでChartGetInteger  でCHART_WIDTH_IN_PIXELSおよびCHART_HEIGHT_IN_PIXELSを取得します。パネル全体の幅と高さ(ヘッダー、グラフ、統計パネル、ギャップを含む)を考慮し、MathMaxおよびMathMinで範囲内にクランプします。currentCanvasXおよびcurrentCanvasYを更新し、ヘッダーおよびグラフのOBJPROP_XDISTANCEおよびOBJPROP_YDISTANCEを更新します。最小化されていない場合は統計パネルのx座標も更新します。

resizingがtrueであり状態が1の場合はリサイズします。マウス差分からnew widthおよびnew heightを計算し、最小幅および最小高さでクランプします。チャートサイズを取得し、現在位置から利用可能な幅と高さを算出します。高さは利用可能領域でクランプし、幅はstatsがある場合は(gapを考慮して/1.5で制約)、そうでなければ全幅で制約します。値が変更された場合、UseBackgroundがtrueなら元画像をコピーしScaleImageでグラフおよびstatsを新サイズへスケールします(statsは幅の半分)。canvasGraphをResizeし、OBJPROP_XSIZEおよびYSIZEを更新します。statsが有効ならcanvasStatsもResizeし、そのx位置を更新します。ヘッダーも全幅に合わせて更新します。その後ヘッダー、グラフ、statsを再描画します。

マウス状態が0でありprev_mouse_stateが1の場合(リリースイベント)では、panel_draggingがtrueならfalseに戻し、CHART_MOUSE_SCROLLをtrueに戻し、再描画します。同様にresizingもfalseにし、スクロールを再有効化し、グラフ更新と再描画をおこないます。最後にlast_mouse_xおよびlast_mouse_yを更新し、prev_mouse_stateを現在のstateに更新します。またティックごとにダッシュボードを更新し、最新価格を反映させる必要があります。

//+------------------------------------------------------------------+
//| Handle tick event                                                |
//+------------------------------------------------------------------+
void OnTick()
{
   static datetime lastBarTime = 0;                              //--- Initialize last time
   datetime currentBarTime = iTime(_Symbol, _Period, 0);         //--- Get current time
   if (currentBarTime > lastBarTime)                             //--- Check new bar
   {
      UpdateGraphOnCanvas();                                     //--- Update graph
      if (EnableStatsPanel) UpdateStatsOnCanvas();               //--- Update stats
      ChartRedraw();                                             //--- Redraw chart
      lastBarTime = currentBarTime;                              //--- Update last time
   }
}

OnTickイベントハンドラでは、まずstaticなlastBarTime(datetime型、初期値0)を用意し、前回のバーのオープン時間を追跡します。現在のバー時間はiTime 関数を用いて、銘柄、時間足、バー0を指定して取得します。現在のバー時間がlastBarTimeより大きい場合、つまり新しいバーが形成された場合に処理します。このときUpdateGraphOnCanvasを呼び出して価格グラフを更新し、EnableStatsPanelがtrueの場合はUpdateStatsOnCanvasも実行して統計情報を更新します。その後ChartRedrawでチャートを再描画し、lastBarTimeを現在の値で更新します。最後に、不要になった描画オブジェクトを削除し、表示の肥大化や不要な要素の蓄積を防ぎます。

//+------------------------------------------------------------------+
//| Deinitialize expert                                              |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   canvasHeader.Destroy();                                       //--- Destroy header
   if (graphCreated) canvasGraph.Destroy();                      //--- Destroy graph if created
   if (statsCreated) canvasStats.Destroy();                      //--- Destroy stats if created
   ChartRedraw();                                                //--- Redraw chart
}

OnDeinitイベントハンドラでは、まずcanvasHeaderをcanvasHeader.Destroyで破棄します。次にgraphCreatedがtrueの場合はcanvasGraph.Destroyを実行してグラフCanvasを破棄し、statsCreatedがtrueの場合はcanvasStats.Destroyで統計Canvasを破棄します。最後にChartRedrawを呼び出してチャートを再描画し、画面上に残っている可能性のある残骸をクリアします。コンパイルすると、次の結果が得られます。

Canvasダッシュボードテスト

可視化から確認できるように、すべてのCanvasコンポーネントが正しく描画され、かつインタラクション可能なCanvasダッシュボードが構築されています。これにより目的は達成されています。残るは、システムの動作確認、つまり前のセクションでおこなったテストです。


バックテスト

テストを実施しました。以下はコンパイル後の可視化を単一のGraphics Interchange Format (GIF)ビットマップ画像形式で示したものです。

Canvas ダッシュボードのバックテスト


結論

結論として、CCanvasクラスを用いたMQL5のCanvasベース価格ダッシュボードを開発しました。このダッシュボードはドラッグ可能かつリサイズ可能なパネル構造を持ち、リアルタイムの価格グラフ(ラインプロットおよび塗りつぶし領域、フォグ効果付き)を表示します。さらにオプションの統計パネルにより、残高や本ダッシュボードはCCanvasクラスを活用し、ドラッグ可能かつリサイズ可能なパネルを提供します。また、最近の価格推移を視覚的に描画し、残高、エクイティなどの口座情報および現在バーのOHLCを表示できます。また、背景画像のサポート、グラデーション、テーマ切替、効率的なイベント処理によるインタラクティブ操作を備えています。システムはスムーズなリサイズのためのバイキュービック補間、オーバーレイ用のアルファブレンディング、新バー更新処理を含み、ネイティブオブジェクトに依存せず柔軟に可視化をおこなうツールとなっています。次回はこのダッシュボードを拡張し、複数テーマに対応したスクロール可能なテキスト表示用Canvasを追加します。これには動的スクロールバーも含まれ、最新版のMetaQuotesターミナルに見られるようなUI表現を実現します。ご期待ください。

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

添付されたファイル |
プライスアクション分析ツールキットの開発(第57回):MQL5による市場状態分類モジュールの開発 プライスアクション分析ツールキットの開発(第57回):MQL5による市場状態分類モジュールの開発
確定済み価格データを用いて価格挙動を解釈するMQL5向けの市場状態分類モジュールを開発および解説します。ボラティリティの収縮および拡大、ならびに構造的一貫性を分析することにより、市場環境をコンプレッション、トランジション、エクスパンション、トレンドとして分類し、プライスアクション分析のための明確な文脈把握の枠組みを提供します。
MQL5におけるイベント駆動型アーキテクチャ:エキスパートアドバイザーを本格的なトレードシステムに進化させる方法 MQL5におけるイベント駆動型アーキテクチャ:エキスパートアドバイザーを本格的なトレードシステムに進化させる方法
MQL5におけるイベント駆動アーキテクチャについて解説し、モノリシックなOnTickモデルから分散処理への移行を取り上げます。定義済みイベントとカスタムイベント、サービス、およびプログラム間のメッセージングについて説明するとともに、アーキテクチャ上でよく見られる典型的な誤りについても考察します。また、実践的な例を通じて、インジケータとEAの連携をどのように構成すれば、負荷を軽減し、可読性を向上させ、保守を容易にできるのかを示します。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
MetaTrader 5とMQL5経済指標カレンダー:ニュースを再現性のあるトレードシステムに変える方法 MetaTrader 5とMQL5経済指標カレンダー:ニュースを再現性のあるトレードシステムに変える方法
MetaTrader 5に組み込まれている経済指標カレンダーを利用したニューストレードの体系的アプローチを紹介します。対象となる内容には、データ構造、API関数、時間同期ルール、イベントフィルタリングが含まれます。また、サーバーへ過度な負荷をかけることなく履歴を管理するためのキャッシュ機構および増分更新方式についても解説します。さらに、同一アルゴリズムを用いた決定論的テストを実現するために、履歴データを.EX5リソースとしてエクスポートする実用的な仕組みも提供します。