MQL5取引ツール(第13回):グラフパネルと統計パネルを備えたCCanvasベースの価格ダッシュボードの実装
はじめに
前回の記事(第12回)では、MetaQuotes Language 5 (MQL5)における相関マトリクスダッシュボードを拡張し、より高いユーザビリティを実現するためのインタラクティブ機能を追加しました。第13回では、価格変動と口座指標について、明確で実践的な判断材料を提供するよう設計された、Canvasベースの価格ダッシュボードを実装します。本ダッシュボードはCCanvasクラスを活用し、ドラッグ可能かつリサイズ可能なパネルを提供します。また、最近の価格推移を視覚的に描画し、残高、エクイティ、現在バーのOHLC (Open/High/Low/Close)などの重要な統計情報も表示します。さらに、背景カスタマイズ、テーマ切り替え、リアルタイム更新などを通じて、取引判断の精度向上を支援します。これらのツールを使用することで、市場の変化をより効率的に監視し、迅速に対応できます。本記事では以下のトピックを扱います。
本記事では、カスタマイズ可能なMQL5 Canvasダッシュボードの完成を目指します。それでは進めます。
CCanvasベースの価格ダッシュボードフレームワークを理解する
Canvasベースの価格ダッシュボードフレームワークは、MQL5のCCanvasクラスを活用してカスタムグラフィカルパネルを作成し、リアルタイム価格データおよび口座指標を表示する仕組みです。これは、標準的なチャートインジケータの代替として機能し、メインチャートを煩雑にすることなく、状況をひと目で把握できる表示を提供するコンパクトかつインタラクティブな手法となっています。本ダッシュボードはCCanvasクラスを活用し、ドラッグ可能かつリサイズ可能なパネルを提供します。また、最近の価格推移を視覚的に描画し、残高、エクイティ、現在バーのOHLC (Open/High/Low/Close)などの口座情報を表示する統計パネルをオプションとして備えています。これらは背景画像のアルファブレンディング、グラデーションまたはソリッドフィル、そして視覚的な二重枠によって補強されます。
拡張機能としては、マウス操作によるドラッグでの再配置、枠線上へのホバーやリサイズグリップを利用したサイズ変更(アイコンによるフィードバック付き)、最小化/最大化の切り替えによるパネルの折りたたみ、ダーク/ライトテーマ切り替えによる配色の動的変更、さらに新規バーに応じたリアルタイム更新が含まれます。
これらの機能では、マウス操作のインタラクションにイベントハンドリングを使用し、画像リサイズを滑らかにするためにバイキュービック補間を採用し、フォグなどのオーバーレイ表現にはアルファブレンディングを用い、透過処理のためにARGB形式による色管理をおこないます。これにより、ダッシュボードはネイティブのMQL5オブジェクトに依存せずに柔軟にサイズ変更できるようになり、これまでに一部ネイティブオブジェクトを使用していた方針から変更し、今回はCanvas機能のみで実装することを主な目的としています。
本設計ではCanvasライブラリを組み込み、位置、サイズ、色、不透明度、描画モードの入力パラメータを定義します。背景画像リソースの読み込みとスケーリングをおこない、ヘッダー、グラフ、統計用にそれぞれ独立したCanvasを作成し、作成時のエラーチェックも実装します。また、ヘッダーではアイコン、ツールチップ、枠線を含む描画関数を実装し、グラフでは価格プロット、塗りつぶし表示、時間ラベル、リサイズ用アイコンなどを描画します。統計セクションでは、テーマ対応テキスト、グラデーション、暗色化した枠線などを表現します。さらに、色補間、色のダークニング、ブレンド処理、ARGB値の抽出などをおこなうヘルパー関数を追加します。そして、ホバー、ドラッグ、リサイズ、切り替えといったチャートイベントを処理し、最小サイズ制限を含むクランプ処理をおこないながら、インタラクションに応じて状態を更新します。最後に、ティック更新時には新しいデータに基づいて描画を更新します。以下に想定されるビジュアル表示の例を示します。

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オブジェクトを描画していきます。その前にヘッダー描画から開始しますが、テーマ対応が必要なため、まずはダークテーマおよびライトテーマの描画に対応するためのテーマヘルパー関数を定義します。
//+------------------------------------------------------------------+ //| 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領域への描画であり、価格の分析結果をラインチャートとして描画します。本質的にはシンプルな価格グラフを自前で構築する処理です。ただしこれはあくまで今回のデモ用として最も簡単な実装を選択しているものであり、必要に応じてより複雑な計算ロジックに置き換えることも可能です。以下がその実装方針です。
//+------------------------------------------------------------------+ //| 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の描画が完了したため、次は右側に配置する統計パネルを作成します。このパネルでは、背景が接する部分で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ダッシュボードが構築されています。これにより目的は達成されています。残るは、システムの動作確認、つまり前のセクションでおこなったテストです。
バックテスト
テストを実施しました。以下はコンパイル後の可視化を単一のGraphics Interchange Format (GIF)ビットマップ画像形式で示したものです。

結論
結論として、CCanvasクラスを用いたMQL5のCanvasベース価格ダッシュボードを開発しました。このダッシュボードはドラッグ可能かつリサイズ可能なパネル構造を持ち、リアルタイムの価格グラフ(ラインプロットおよび塗りつぶし領域、フォグ効果付き)を表示します。さらにオプションの統計パネルにより、残高や本ダッシュボードはCCanvasクラスを活用し、ドラッグ可能かつリサイズ可能なパネルを提供します。また、最近の価格推移を視覚的に描画し、残高、エクイティなどの口座情報および現在バーのOHLCを表示できます。また、背景画像のサポート、グラデーション、テーマ切替、効率的なイベント処理によるインタラクティブ操作を備えています。システムはスムーズなリサイズのためのバイキュービック補間、オーバーレイ用のアルファブレンディング、新バー更新処理を含み、ネイティブオブジェクトに依存せず柔軟に可視化をおこなうツールとなっています。次回はこのダッシュボードを拡張し、複数テーマに対応したスクロール可能なテキスト表示用Canvasを追加します。これには動的スクロールバーも含まれ、最新版のMetaQuotesターミナルに見られるようなUI表現を実現します。ご期待ください。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/21038
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
プライスアクション分析ツールキットの開発(第57回):MQL5による市場状態分類モジュールの開発
MQL5におけるイベント駆動型アーキテクチャ:エキスパートアドバイザーを本格的なトレードシステムに進化させる方法
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
MetaTrader 5とMQL5経済指標カレンダー:ニュースを再現性のあるトレードシステムに変える方法
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索