MQL5取引ツール(第19回):チャート描画用のインタラクティブなツールパレットの構築
はじめに
前回の第18回では、MetaQuotes Language 5 (MQL5)において、ベクターベースの矩形と三角形を組み合わせることで、向きを制御できる角丸の吹き出しを作成し、トレードインターフェース向けの動的なUI要素を実現しました。第19回となる今回は、ドラッグ可能なパネル、リサイズ機能、テーマ切り替え、および十字カーソル、ライン、図形などのツール用ボタンを備えた、チャート描画用のインタラクティブなツールパレットを開発します。このカスタマイズ可能なシステムは、リアルタイムな操作と操作ガイドを提供することで、分析作業を効率化します。本記事では以下のトピックを扱います。
本記事を読み終える頃には、トレードワークフローの拡張に活用できる、実用的なツールパレットが完成します。それでは始めましょう。
チャート向けインタラクティブツールパレットの設計
チャート描画用のインタラクティブなツールパレットは、十字カーソル、トレンドライン、ライン、矩形、フィボナッチ、テキスト、矢印などの主要な描画機能をボタンとして一元化し、トレードチャート上で素早く選択・適用できるようにします。また、リアルタイムなフィードバックを提供することで、操作性を向上させます。さらに、このツールパレットは、ドラッグによる移動、リサイズ、ダークテーマとライトテーマの切り替え、および作業領域を効率的に活用するための最小化機能をサポートします。加えて、操作ガイドやステータス更新を表示することで、直感的な操作を実現します。この設計により、チャートを煩雑にすることなく、ユーザーの操作に応じて柔軟に適応する、カスタマイズ可能で応答性の高いUIを提供し、分析作業を効率化します。実装では、ヘッダーおよびパネルの描画にキャンバスオブジェクトを使用し、インタラクティブな操作を実現するためにマウスイベントを管理します。また、リサイズモードやツールを表す列挙型を定義し、チャート上へのオブジェクト生成によって描画処理を実装します。ここでは、目標とするシステムの概要を視覚的に示します。

MQL5での実装
MQL5でプログラムを作成するには、まずMetaEditorを開き、ナビゲーターで[Experts]フォルダを探します。[新規]タブをクリックして指示に従い、ファイルを作成します。ファイルが作成されたら、コーディング環境で、まずプログラム全体で使用する入力パラメータとグローバル変数をいくつか宣言する必要があります。
//+------------------------------------------------------------------+ //| Tools Palette.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 canvasHeader; //--- Declare header canvas CCanvas canvasTools; //--- Declare tools canvas string canvasHeaderName = "HeaderCanvas"; //--- Set header canvas name string canvasToolsName = "ToolsCanvas"; //--- Set tools canvas name input int CanvasX = 20; // Panel X position input int CanvasY = 30; // Panel Y position input int CanvasWidth = 350; // Panel width input int CanvasHeight = 200; // Panel height input double BackgroundOpacity = 0.8; // Background opacity bool is_dark_theme = true; //--- Set initial dark theme flag color DarkHeaderColor = C'60,60,60'; //--- Set dark header color color DarkHeaderHoverColor = clrRed; //--- Set dark header hover color color DarkHeaderDragColor = clrMediumBlue; //--- Set dark header drag color color DarkHeaderTextColor = clrWhite; //--- Set dark header text color color DarkBorderColor = clrBlack; //--- Set dark border color color DarkTopColor = clrBlack; //--- Set dark top color color DarkIconColor = clrWhite; //--- Set dark icon color color LightHeaderColor = clrSilver; //--- Set light header color color LightHeaderHoverColor = clrRed; //--- Set light header hover color color LightHeaderDragColor = clrMediumBlue; //--- Set light header drag color color LightHeaderTextColor = clrBlack; //--- Set light header text color color LightBorderColor = clrBlack; //--- Set light border color color LightTopColor = clrWhite; //--- Set light top color color LightIconColor = clrBlack; //--- Set light icon color color DarkInstructionColor = clrLime; //--- Set dark instruction color color LightInstructionColor = clrGreen; //--- Set light instruction color color DarkStatusColor = clrMagenta; //--- Set dark status color color LightStatusColor = clrPurple; //--- Set light status color color DarkActiveBtnColor = clrGreen; //--- Set dark active button color color LightActiveBtnColor = clrDarkGreen; //--- Set light active button color color DarkHoverBtnColor = clrDarkGray; //--- Set dark hover button color color LightHoverBtnColor = clrLightGray; //--- Set light hover button color int currentCanvasX = CanvasX; //--- Initialize current X position int currentCanvasY = CanvasY; //--- Initialize current Y position int currentWidth = CanvasWidth; //--- Initialize current width int currentHeight = CanvasHeight; //--- Initialize current height bool panel_dragging = false; //--- Initialize panel dragging flag int panel_drag_x = 0, panel_drag_y = 0; //--- Initialize drag coordinates int panel_start_x = 0, panel_start_y = 0; //--- Initialize start coordinates bool resizing = false; //--- Initialize resizing flag enum ENUM_RESIZE_MODE { NONE, // No resize BOTTOM, // Bottom resize RIGHT, // Right resize BOTTOM_RIGHT // Bottom-right resize }; ENUM_RESIZE_MODE resize_mode = NONE; //--- Initialize resize mode ENUM_RESIZE_MODE hover_mode = NONE; //--- Initialize hover mode 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 thickness const int min_width = 150; //--- Set minimum width const int min_height = 200; //--- Set minimum height int hover_mouse_local_x = 0; //--- Initialize local hover X int hover_mouse_local_y = 0; //--- Initialize local hover Y bool header_hovered = false; //--- Initialize header hover flag bool minimize_hovered = false; //--- Initialize minimize hover flag bool close_hovered = false; //--- Initialize close hover flag bool theme_hovered = false; //--- Initialize theme hover flag bool resize_hovered = false; //--- Initialize resize hover flag int prev_mouse_state = 0; //--- Initialize previous mouse state int header_height = 27; //--- Set header height int button_size = 25; //--- Set button size int theme_x_offset = -75; //--- Set theme X offset int minimize_x_offset = -50; //--- Set minimize X offset int close_x_offset = -25; //--- Set close X offset bool panel_minimized = false; //--- Initialize minimized flag enum TOOL_TYPE { TOOL_NONE, // No tool TOOL_CROSSHAIR, // Crosshair tool TOOL_TRENDLINE, // Trendline tool TOOL_HLINE, // Horizontal line tool TOOL_VLINE, // Vertical line tool TOOL_RECTANGLE, // Rectangle tool TOOL_FIBO, // Fibonacci tool TOOL_TEXT, // Text tool TOOL_ARROW // Arrow tool }; TOOL_TYPE active_tool = TOOL_NONE; //--- Initialize active tool bool drawing_first_click = false; //--- Initialize first click flag datetime draw_time1 = 0; //--- Initialize draw time 1 double draw_price1 = 0.0; //--- Initialize draw price 1 long local_chart_id = 0; //--- Initialize local chart ID string current_instruction = ""; //--- Initialize current instruction struct ToolButton { // Define tool button structure string symbol; // Store symbol string font; // Store font TOOL_TYPE type; // Store type string tooltip; // Store tooltip }; ToolButton buttons[8]; //--- Declare buttons array int hovered_tool_index = -1; //--- Initialize hovered tool index bool measuring = false; //--- Initialize measuring flag datetime fixed_time = 0; //--- Initialize fixed time double fixed_price = 0.0; //--- Initialize fixed price ulong last_click_time = 0; //--- Initialize last click time const int tools_y_offset = -1; //--- Set tools Y offset
まず、パレットを構築するためのコアグラフィック機能にアクセスするために、「#include <Canvas/Canvas.mqh>」を使用してCanvasライブラリを含めることから実装を開始します。次に、パネルのヘッダーと本体を描画するためのグローバルなキャンバスオブジェクトとしてcanvasHeaderとcanvasToolsを宣言し、それぞれに「HeaderCanvas」と「ToolsCanvas」の名前を割り当てて参照できるようにします。また、位置、サイズ、背景の不透明度を設定する入力パラメータを定義し、ユーザーがパレットの表示を自由にカスタマイズできるようにします。テーマ切り替えに対応するため、is_dark_themeをtrueに初期化し、ダークテーマとライトテーマのそれぞれについて、ヘッダー、境界線、アイコン、操作ガイド、ステータス表示、およびボタンに使用する色定数を定義します。これにより、テーマを切り替えても一貫した外観を維持できます。
パネルの状態は、現在の位置とサイズを保持する変数、ドラッグ操作用のフラグ、ENUM_RESIZE_MODE列挙型(NONE、BOTTOM、RIGHT、BOTTOM_RIGHT)によるリサイズモード、ホバー状態、および操作性を確保するための最小サイズや判定領域の厚さなどの定数によって管理します。描画ツールについては、NONEからARROWまでのツールを定義したTOOL_TYPE列挙型を用意し、active_toolをNONEに初期化します。また、drawing_first_clickなどの描画状態を管理するフラグも用意します。さらに、シンボル、フォント、ツール種別、およびツールチップを保持するToolButton構造体を定義し、ツールパレットの各項目を格納するbuttons[8]配列を作成します。最後に、計測モード、固定ポイント、クリック時間、ホバー中のボタンインデックス、およびツールの配置位置を調整するためのY方向オフセットなどの変数を用意し、インタラクティブな機能を実装するための準備を整えます。続いて、コードをモジュール化して保守性を高めるために、いくつかのヘルパー関数を実装します。
//+------------------------------------------------------------------+ //| Get theme-aware colors | //+------------------------------------------------------------------+ color GetHeaderColor() { return is_dark_theme ? DarkHeaderColor : LightHeaderColor; } //--- Return header color based on theme color GetHeaderHoverColor() { return is_dark_theme ? DarkHeaderHoverColor : LightHeaderHoverColor; } //--- Return header hover color based on theme color GetHeaderDragColor() { return is_dark_theme ? DarkHeaderDragColor : LightHeaderDragColor; } //--- Return header drag color based on theme color GetHeaderTextColor() { return is_dark_theme ? DarkHeaderTextColor : LightHeaderTextColor; } //--- Return header text color based on theme color GetBorderColor() { return is_dark_theme ? DarkBorderColor : LightBorderColor; } //--- Return border color based on theme color GetTopColor() { return is_dark_theme ? DarkTopColor : LightTopColor; } //--- Return top color based on theme color GetIconColor() { return is_dark_theme ? DarkIconColor : LightIconColor; } //--- Return icon color based on theme color GetInstructionColor() { return is_dark_theme ? DarkInstructionColor : LightInstructionColor; } //--- Return instruction color based on theme color GetStatusColor() { return is_dark_theme ? DarkStatusColor : LightStatusColor; } //--- Return status color based on theme color GetActiveBtnColor() { return is_dark_theme ? DarkActiveBtnColor : LightActiveBtnColor; } //--- Return active button color based on theme color GetHoverBtnColor() { return is_dark_theme ? DarkHoverBtnColor : LightHoverBtnColor; } //--- Return hover button color based on theme //+------------------------------------------------------------------+ //| Get tool name from type | //+------------------------------------------------------------------+ string GetToolName(TOOL_TYPE t) { for (int i = 0; i < 8; i++) { //--- Loop over buttons if (buttons[i].type == t) { //--- Check if type matches return StringSubstr(buttons[i].tooltip, 0, StringFind(buttons[i].tooltip, " Tool")); //--- Return tool name } } return "None"; //--- Return default if no match } //+------------------------------------------------------------------+ //| Check if tool is single-click | //+------------------------------------------------------------------+ bool IsSingleClickTool(TOOL_TYPE t) { switch(t) { //--- Switch on tool type case TOOL_HLINE: //--- Handle horizontal line case TOOL_VLINE: //--- Handle vertical line case TOOL_TEXT: //--- Handle text case TOOL_ARROW: //--- Handle arrow return true; //--- Return true for single-click tools default: //--- Handle default return false; //--- Return false otherwise } }
続いて、GetHeaderColorのような一連のgetter関数を定義し、テーマに応じた色を取得できるようにします。これらの関数は、is_dark_themeフラグに基づいてダークテーマまたはライトテーマ用の色を返すため、ヘッダー、ホバー状態、ドラッグ状態、テキスト、境界線、アイコン、操作ガイド、ステータス表示、アクティブなボタン、およびホバー時の色を、一貫したスタイルで管理できます。また、各所でテーマ判定を繰り返す必要がなくなります。次に、GetToolName関数を実装します。この関数は、buttons配列を走査して指定されたツール種別と一致する項目を検索し、対応するツール名を取得します。取得したツールチップから「 Tool」までの部分を切り出して、表示用の名称を返します。一致する項目が見つからない場合は、Noneを返します。ツールの種類を判定するために、IsSingleClickTool関数も実装します。この関数は、ツール種別に対してswitch文を使用し、水平線、垂直線、テキスト、および矢印のように1回のクリックだけで配置できるツールについてはtrueを返し、それ以外のツールについてはfalseを返します。これにより、各ツールに応じた適切なイベント処理をおこなえるようになります。これらの準備が整ったら、ヘッダーとツールパレット本体の作成を初期化していきます。
//+------------------------------------------------------------------+ //| Draw header | //+------------------------------------------------------------------+ void DrawHeader() { canvasHeader.Erase(0); //--- Erase header canvas color header_bg = panel_dragging ? GetHeaderDragColor() : (header_hovered ? GetHeaderHoverColor() : GetHeaderColor()); //--- Get header background color canvasHeader.FillRectangle(0, 0, currentWidth - 1, header_height - 1, ColorToARGB(header_bg, 255)); //--- Fill header rectangle uint argbBorder = ColorToARGB(GetBorderColor(), 255); //--- Get border ARGB canvasHeader.Rectangle(0, 0, currentWidth - 1, header_height - 1, argbBorder); //--- Draw border rectangle canvasHeader.FontSet("Arial Bold", 13); //--- Set font for title canvasHeader.TextOut(10, (header_height - 13)/2, "Tools Palette", ColorToARGB(GetHeaderTextColor(), 255)); //--- Draw title text int button_y = (header_height - 20)/2; //--- Compute button Y position int theme_button_x = currentWidth + theme_x_offset; //--- Compute theme button X canvasHeader.FontSet("Wingdings", 20); //--- Set font for theme symbol string theme_symbol = CharToString(91); //--- Set theme symbol int theme_w = canvasHeader.TextWidth(theme_symbol); //--- Get theme symbol width int theme_x = theme_button_x + (button_size - theme_w) / 2; //--- Compute theme X position color theme_color = theme_hovered ? clrYellow : GetHeaderTextColor(); //--- Get theme color canvasHeader.TextOut(theme_x, button_y, theme_symbol, ColorToARGB(theme_color, 255)); //--- Draw theme symbol int min_button_x = currentWidth + minimize_x_offset; //--- Compute minimize button X string min_symbol = panel_minimized ? CharToString(111) : CharToString(114); //--- Set minimize symbol int min_w = canvasHeader.TextWidth(min_symbol); //--- Get minimize symbol width int min_x = min_button_x + (button_size - min_w) / 2; //--- Compute minimize X position color min_color = minimize_hovered ? clrYellow : GetHeaderTextColor(); //--- Get minimize color canvasHeader.TextOut(min_x, button_y, min_symbol, ColorToARGB(min_color, 255)); //--- Draw minimize symbol int close_button_x = currentWidth + close_x_offset; //--- Compute close button X canvasHeader.FontSet("Webdings", 20); //--- Set font for close symbol string close_symbol = CharToString(114); //--- Set close symbol int close_w = canvasHeader.TextWidth(close_symbol); //--- Get close symbol width int close_x = close_button_x + (button_size - close_w) / 2; //--- Compute close X position color close_color = close_hovered ? clrRed : GetHeaderTextColor(); //--- Get close color canvasHeader.TextOut(close_x, button_y, close_symbol, ColorToARGB(close_color, 255)); //--- Draw close symbol canvasHeader.Update(); //--- Update header canvas } //+------------------------------------------------------------------+ //| Draw tools panel | //+------------------------------------------------------------------+ void DrawToolsPanel() { canvasTools.Erase(0); //--- Erase tools canvas uint argb_fill = ColorToARGB(GetTopColor(), (uchar)(255 * BackgroundOpacity)); //--- Get fill ARGB canvasTools.FillRectangle(0, 0, currentWidth - 1, currentHeight - 1, argb_fill); //--- Fill background rectangle uint argb_border = ColorToARGB(GetBorderColor(), 255); //--- Get border ARGB canvasTools.Rectangle(0, 0, currentWidth - 1, currentHeight - 1, argb_border); //--- Draw border rectangle int columns = 4; //--- Set number of columns int padding = 5; //--- Set padding int button_width = (currentWidth - (columns + 1) * padding) / columns; //--- Compute button width int button_height = 70; //--- Set button height int start_x = padding; //--- Set start X int start_y = padding; //--- Set start Y for (int i = 0; i < 8; i++) { //--- Loop over buttons int col = i % columns; //--- Compute column int row = i / columns; //--- Compute row int x = start_x + col * (button_width + padding); //--- Compute X position int y = start_y + row * (button_height + padding); //--- Compute Y position color btn_bg = (active_tool == buttons[i].type) ? GetActiveBtnColor() : ((i == hovered_tool_index) ? GetHoverBtnColor() : clrNONE); //--- Get button background if (btn_bg != clrNONE) canvasTools.FillRectangle(x, y, x + button_width - 1, y + button_height - 1, ColorToARGB(btn_bg, 255)); //--- Fill button rectangle if needed canvasTools.FontSet(buttons[i].font, 35); //--- Set font for icon int text_w = canvasTools.TextWidth(buttons[i].symbol); //--- Get icon width int text_h = canvasTools.TextHeight(buttons[i].symbol); //--- Get icon height int icon_x = x + (button_width - text_w) / 2; //--- Compute icon X int icon_y = y + 5; //--- Set icon Y canvasTools.TextOut(icon_x, icon_y, buttons[i].symbol, ColorToARGB(GetIconColor(), 255)); //--- Draw icon string tool_name = StringSubstr(buttons[i].tooltip, 0, StringFind(buttons[i].tooltip, " Tool")); //--- Get tool name canvasTools.FontSet("Arial Bold", 12); //--- Set font for name int name_w = canvasTools.TextWidth(tool_name); //--- Get name width canvasTools.TextOut(x + (button_width - name_w) / 2, y + button_height - 20, tool_name, ColorToARGB(GetIconColor(), 255)); //--- Draw tool name } if (StringLen(current_instruction) > 0) { //--- Check if instruction exists canvasTools.FontSet("Arial", 14); //--- Set font for instruction int instr_w = canvasTools.TextWidth(current_instruction); //--- Get instruction width int instr_x = (currentWidth - instr_w) / 2; //--- Compute instruction X canvasTools.TextOut(instr_x, currentHeight - 30, current_instruction, ColorToARGB(GetInstructionColor(), 255)); //--- Draw instruction } string status_text = "Active: " + GetToolName(active_tool); //--- Set status text canvasTools.FontSet("Arial Bold", 14); //--- Set font for status int status_w = canvasTools.TextWidth(status_text); //--- Get status width int status_x = (currentWidth - status_w) / 2; //--- Compute status X canvasTools.TextOut(status_x, currentHeight - 15, status_text, ColorToARGB(GetStatusColor(), 255)); //--- Draw status if (resize_hovered || resizing) { //--- Check if resize hovered or resizing string icon_font = "Wingdings 3"; //--- Set icon font int icon_size = 25; //--- Set icon size uchar icon_code = 0; //--- Initialize icon code int angle = 0; //--- Initialize angle switch (hover_mode) { //--- Switch on hover mode case BOTTOM: icon_code = (uchar)'2'; angle = 0; break; //--- Set for bottom case RIGHT: icon_code = (uchar)'1'; angle = 0; break; //--- Set for right case BOTTOM_RIGHT: icon_code = (uchar)'2'; angle = 450; break; //--- Set for bottom-right } if (icon_code != 0) { //--- Check if icon code set canvasTools.FontSet(icon_font, icon_size); //--- Set font for icon canvasTools.FontAngleSet(angle); //--- Set font angle color icon_color = (resize_hovered || resizing) ? clrRed : GetIconColor(); //--- Get icon color int icon_x = (hover_mode == BOTTOM) ? MathMin(MathMax(hover_mouse_local_x - icon_size/2, 0), currentWidth - icon_size) : currentWidth - icon_size - 2; //--- Compute icon X int icon_y = (hover_mode == RIGHT) ? MathMin(MathMax(hover_mouse_local_y - icon_size/2, 0), currentHeight - icon_size) : currentHeight - icon_size - 2; //--- Compute icon Y if (hover_mode == BOTTOM_RIGHT) { icon_x = currentWidth - icon_size - 10; icon_y = currentHeight - icon_size; } //--- Adjust for bottom-right canvasTools.TextOut(icon_x, icon_y, CharToString(icon_code), ColorToARGB(icon_color, 255)); //--- Draw icon canvasTools.FontAngleSet(0); //--- Reset font angle } } canvasTools.Update(); //--- Update tools canvas }
初期レンダリングを作成するために、DrawHeader関数を実装してパレットのヘッダーをキャンバス上に描画します。処理は、Eraseを0に設定して既存の内容を消去することから開始します。その後、ドラッグ中の場合はGetHeaderDragColor、ホバー中の場合はGetHeaderHoverColor、それ以外の場合は標準カラーを取得し、動的な背景色をARGB形式に変換して矩形を塗りつぶします。次に、GetBorderColorから取得した色をARGBに変換し、Rectangleを使用してボーダーを描画します。タイトル表示用には太字の"Arial"フォントを設定し、テーマのテキストカラーを使用してFontSetとTextOutによりタイトル「Tools Palette」を描画します。コントロールを追加するために、まずボタンのY座標が中央揃えになるよう計算します。テーマボタンについては、Wingdingsフォントを設定し、ホバー状態に応じて「[」の位置と色を調整し、TextOutで描画します。同様に、最小化ボタンでは'o'または「r」を切り替えて表示し、閉じるボタンではWebdingsフォントを使用して「X」を描画します。各ボタンはホバー状態に応じて、黄色や赤などの色に変更します。最後にUpdateを呼び出してキャンバスを更新し、変更内容を表示します。必要に応じて、アイコンには独自の文字を使用してカスタマイズできます。使用可能なフォント文字一覧については、フォントシンボルの一覧を参照してください。

DrawToolsPanel関数では、まずツールパレット用のキャンバスを消去し、BackgroundOpacityを反映したARGB値を使用して背景色で塗りつぶした後、外枠の矩形を描画します。続いて、ボタンを4列に配置し、余白を考慮しながらレイアウトをおこないます。ループ処理では、ボタンがアクティブまたはホバー状態の場合に、GetActiveBtnColorまたはGetHoverBtnColorを使用して背景を描画します。その後、「Wingdings」の「v」のようなアイコンには専用フォントを設定し、TextOutを使用してアイコンカラーで中央に描画します。さらに、その下にツール名を太字で表示します。current_instructionに操作ガイドが設定されている場合は、GetInstructionColorを使用して、パネル下部中央付近にガイドを描画します。同様に、現在アクティブなツールの状態も表示します。リサイズ操作を示すために、resize_hoveredまたはresizingがtrueの場合は、「Wingdings 3」フォントを設定し、hover_modeに応じて矢印コードと回転角度を決定します。例えば、右下方向のリサイズでは文字「2」を450度の角度で使用します。矢印の描画位置は現在の状態に応じて動的に決定し、リサイズ中またはリサイズ可能な状態であることを示すため、赤色またはアイコンカラーで描画した後、回転角度を元に戻します。最後に、Updateを呼び出してキャンバスを更新し、描画内容を反映します。これでツールパレット本体の描画処理が完成したので、次は以下のロジックを使用して、このパレットを初期化および描画します。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { currentCanvasX = CanvasX; //--- Set current X from input currentCanvasY = CanvasY; //--- Set current Y from input currentWidth = CanvasWidth; //--- Set current width from input currentHeight = CanvasHeight; //--- Set current height from input if (!canvasHeader.CreateBitmapLabel(0, 0, canvasHeaderName, currentCanvasX, currentCanvasY, currentWidth, header_height, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create header canvas Print("Failed to create Header Canvas"); //--- Print failure message return(INIT_FAILED); //--- Return initialization failure } if (!canvasTools.CreateBitmapLabel(0, 0, canvasToolsName, currentCanvasX, currentCanvasY + header_height + tools_y_offset, currentWidth, currentHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create tools canvas Print("Failed to create Tools Canvas"); //--- Print failure message return(INIT_FAILED); //--- Return initialization failure } buttons[0].symbol = CharToString('v'); buttons[0].font = "Wingdings"; buttons[0].type = TOOL_CROSSHAIR; buttons[0].tooltip = "Crosshair Tool"; //--- Initialize button 0 buttons[1].symbol = CharToString('.'); buttons[1].font = "Wingdings 3"; buttons[1].type = TOOL_TRENDLINE; buttons[1].tooltip = "Trendline Tool"; //--- Initialize button 1 buttons[2].symbol = CharToString('*'); buttons[2].font = "Wingdings 3"; buttons[2].type = TOOL_HLINE; buttons[2].tooltip = "Horizontal Line Tool"; //--- Initialize button 2 buttons[3].symbol = CharToString('+'); buttons[3].font = "Wingdings 3"; buttons[3].type = TOOL_VLINE; buttons[3].tooltip = "Vertical Line Tool"; //--- Initialize button 3 buttons[4].symbol = CharToString('c'); buttons[4].font = "Webdings"; buttons[4].type = TOOL_RECTANGLE; buttons[4].tooltip = "Rectangle Tool"; //--- Initialize button 4 buttons[5].symbol = CharToString('"'); buttons[5].font = "Webdings"; buttons[5].type = TOOL_FIBO; buttons[5].tooltip = "Fibonacci Tool"; //--- Initialize button 5 buttons[6].symbol = CharToString('>'); buttons[6].font = "Webdings"; buttons[6].type = TOOL_TEXT; buttons[6].tooltip = "Text Tool"; //--- Initialize button 6 buttons[7].symbol = CharToString(225); buttons[7].font = "Wingdings"; buttons[7].type = TOOL_ARROW; buttons[7].tooltip = "Arrow Tool"; //--- Initialize button 7 DrawHeader(); //--- Draw header DrawToolsPanel(); //--- Draw tools panel ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); //--- Enable mouse move events ChartRedraw(); //--- Redraw chart local_chart_id = ChartID(); //--- Set local chart ID return(INIT_SUCCEEDED); //--- Return initialization success }
続いて、OnInitイベントハンドラで初期化処理を実装します。まず、ユーザー入力で指定された値を基に、現在のパネル位置とサイズを初期化し、設定内容と一致するようにします。次に、透過処理のためにARGB値を正規化してCreateBitmapLabelを使用し、ヘッダー用キャンバスを作成します。作成に失敗した場合はエラーログを出力し、INIT_FAILEDを返して初期化を終了します。同様に、Y方向のオフセットを考慮してヘッダーの下に配置されるツールパレット用キャンバスも作成します。ツールパレットの内容を構成するために、buttons配列の各要素を初期化します。各要素には、十字カーソル用の「v」のようなシンボル、Wingdingsなどのフォント、列挙型で定義したツール種別、およびホバー時に表示するツールチップを設定します。その後、それぞれの描画関数を呼び出してヘッダーとツールパレットを描画します。さらに、ChartSetIntegerを使用してマウス移動イベントを有効化し、ユーザー操作を取得できるようにします。最後に、チャートを再描画し、オブジェクト管理のためにローカルチャートIDを保存して、INIT_SUCCEEDEDを返します。コンパイルすると、次の結果が得られます。

パネルが正しく初期化されていることが確認できます。次に、このパネルにインタラクティブな機能とテーマ切り替え機能を実装していきます。そのために、実装を容易にするため、さらにいくつかの補助関数を定義します。
//+------------------------------------------------------------------+ //| Toggle theme | //+------------------------------------------------------------------+ void ToggleTheme() { is_dark_theme = !is_dark_theme; //--- Toggle theme flag DrawHeader(); //--- Redraw header DrawToolsPanel(); //--- Redraw tools panel ChartRedraw(); //--- Redraw chart } //+------------------------------------------------------------------+ //| Toggle minimize | //+------------------------------------------------------------------+ void ToggleMinimize() { panel_minimized = !panel_minimized; //--- Toggle minimized flag if (panel_minimized) { //--- Check if minimized canvasTools.Destroy(); //--- Destroy tools canvas } else { //--- Handle restored canvasTools.CreateBitmapLabel(0, 0, canvasToolsName, currentCanvasX, currentCanvasY + header_height + tools_y_offset, currentWidth, currentHeight, COLOR_FORMAT_ARGB_NORMALIZE); //--- Recreate tools canvas DrawToolsPanel(); //--- Redraw tools panel } DrawHeader(); //--- Redraw header ChartRedraw(); //--- Redraw chart } //+------------------------------------------------------------------+ //| Is mouse over header excluding buttons | //+------------------------------------------------------------------+ bool IsMouseOverHeader(int mx, int my) { if (mx < currentCanvasX || mx > currentCanvasX + currentWidth || my < currentCanvasY || my > currentCanvasY + header_height) return false; //--- Check outside header int btn_left = currentCanvasX + currentWidth + theme_x_offset; //--- Compute buttons left if (mx >= btn_left && mx <= btn_left + button_size * 3) return false; //--- Check over buttons return true; //--- Return true if over header } //+------------------------------------------------------------------+ //| Is mouse over specific button | //+------------------------------------------------------------------+ bool IsMouseOverButton(int mx, int my, int offset) { int btn_left = currentCanvasX + currentWidth + offset; //--- Compute button left return (mx >= btn_left && mx <= btn_left + button_size && my >= currentCanvasY && my <= currentCanvasY + header_height); //--- Return if over button } //+------------------------------------------------------------------+ //| Is mouse over resize area | //+------------------------------------------------------------------+ bool IsMouseOverResize(int mx, int my, ENUM_RESIZE_MODE &rm) { if (panel_minimized) return false; //--- Return false if minimized int tx = currentCanvasX; //--- Set tools X int ty = currentCanvasY + header_height + tools_y_offset; //--- Set tools Y int tr = tx + currentWidth; //--- Set right int tb = ty + currentHeight; //--- Set bottom bool over_right = mx >= tr - resize_thickness && mx <= tr && my >= ty && my <= tb; //--- Check over right bool over_bottom = my >= tb - resize_thickness && my <= tb && mx >= tx && mx <= tr; //--- Check over bottom if (over_bottom && over_right) { rm = BOTTOM_RIGHT; return true; } //--- Set and return bottom-right if (over_bottom) { rm = BOTTOM; return true; } //--- Set and return bottom if (over_right) { rm = RIGHT; return true; } //--- Set and return right return false; //--- Return false otherwise } //+------------------------------------------------------------------+ //| Get hovered tool | //+------------------------------------------------------------------+ int GetHoveredTool(int mx, int my) { int lx = mx - currentCanvasX; //--- Compute local X int ly = my - (currentCanvasY + header_height + tools_y_offset); //--- Compute local Y if (lx < 0 || lx > currentWidth || ly < 0 || ly > currentHeight) return -1; //--- Return -1 if outside int columns = 4; //--- Set columns int padding = 5; //--- Set padding int button_width = (currentWidth - (columns + 1) * padding) / columns; //--- Compute button width int button_height = 70; //--- Set button height for (int i = 0; i < 8; i++) { //--- Loop over buttons int col = i % columns; //--- Compute column int row = i / columns; //--- Compute row int x = padding + col * (button_width + padding); //--- Compute X int y = padding + row * (button_height + padding); //--- Compute Y if (lx >= x && lx < x + button_width && ly >= y && ly < y + button_height) return i; //--- Return index if over } return -1; //--- Return -1 if none } //+------------------------------------------------------------------+ //| Toggle tool activation | //+------------------------------------------------------------------+ void ToggleTool(TOOL_TYPE type) { if (active_tool == type) { //--- Check if already active if (active_tool == TOOL_CROSSHAIR) { //--- Handle crosshair deactivation DeleteCrosshair(); //--- Delete crosshair measuring = false; //--- Reset measuring ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enable scroll } active_tool = TOOL_NONE; //--- Deactivate tool drawing_first_click = false; //--- Reset first click current_instruction = ""; //--- Clear instruction } else { //--- Handle activation if (active_tool == TOOL_CROSSHAIR) { //--- Deactivate previous crosshair DeleteCrosshair(); //--- Delete crosshair measuring = false; //--- Reset measuring ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enable scroll } active_tool = type; //--- Set active tool drawing_first_click = false; //--- Reset first click if (active_tool == TOOL_CROSSHAIR) { //--- Handle crosshair instruction current_instruction = "Move mouse for crosshair. Double-click to start measuring."; //--- Set instruction } else { //--- Handle other tools current_instruction = "Click on chart to draw."; //--- Set instruction } } DrawToolsPanel(); //--- Redraw tools panel ChartRedraw(); //--- Redraw chart } //+------------------------------------------------------------------+ //| Deactivate current tool on Escape | //+------------------------------------------------------------------+ void DeactivateTool() { if (active_tool == TOOL_CROSSHAIR) { //--- Check if crosshair active DeleteCrosshair(); //--- Delete crosshair measuring = false; //--- Reset measuring ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enable scroll } active_tool = TOOL_NONE; //--- Deactivate tool drawing_first_click = false; //--- Reset first click current_instruction = ""; //--- Clear instruction DrawToolsPanel(); //--- Redraw tools panel ChartRedraw(); //--- Redraw chart } //+------------------------------------------------------------------+ //| Delete measure objects | //+------------------------------------------------------------------+ void DeleteMeasureObjects() { ObjectDelete(local_chart_id, "Cross_HF"); //--- Delete fixed horizontal ObjectDelete(local_chart_id, "Cross_VF"); //--- Delete fixed vertical ObjectDelete(local_chart_id, "Measure_Line"); //--- Delete measure line ObjectDelete(local_chart_id, "Measure_Label"); //--- Delete measure label } //+------------------------------------------------------------------+ //| Delete crosshair | //+------------------------------------------------------------------+ void DeleteCrosshair() { ObjectDelete(local_chart_id, "Cross_H"); //--- Delete moving horizontal ObjectDelete(local_chart_id, "Cross_V"); //--- Delete moving vertical DeleteMeasureObjects(); //--- Delete measure objects ChartRedraw(); //--- Redraw chart }
ここでは、ToggleTheme関数を実装し、ダークモードとライトモードを切り替えます。is_dark_themeフラグの値を反転させることでテーマを変更し、その後ヘッダーとツールパネルを再描画して新しい配色を適用します。最後にチャートを更新し、変更内容を即座に表示します。次に、ToggleMinimize関数を作成し、panel_minimizedによってパネル状態を切り替えます。最小化時には、表示領域を節約するためにツールキャンバスを削除し、復元時には再作成します。その後、ヘッダーを再描画してチャートを更新します。ユーザー操作を検出するために、IsMouseOverHeader関数を定義します。この関数では、マウス位置がヘッダー領域内にあるかを確認します。ただし、テーマ切り替えボタンの配置領域(theme_offsetから始まる部分)は除外し、ドラッグ操作が可能な領域の場合のみtrueを返します。
同様に、IsMouseOverButton関数では、特定のボタン上にマウスがあるかを判定します。オフセットからボタンの左端位置を計算し、ボタンサイズおよびヘッダーの高さと比較することで、対象ボタン上にあるかを確認します。リサイズ処理については、IsMouseOverResize関数を使用します。この関数では、ツールパネルの下端、右端、または右下隅付近にマウスがあるかを判定します(最小化状態の場合は無視します)。検出された場合は、リサイズモードをBOTTOMRIGHTなどに設定し、対象領域内であればtrueを返します。さらに、GetHoveredTool関数を追加し、マウス位置の下にあるボタンを特定します。マウス座標をローカル座標へ変換し、余白を考慮した4列グリッドを走査します。ボタンの範囲内にある場合は対応するインデックスを返し、それ以外の場合は-1を返します。
ツール管理のために、ToggleTool関数ではツール種別に応じて有効化または無効化します。ツール切り替え時には、必要に応じて十字カーソル関連のオブジェクトを削除し、描画状態フラグをリセットします。また、「Click on chart to draw」のような適切な操作ガイドを設定してから、パネルを再描画します。DeactivateTool関数では、現在有効なツールを完全にリセットします。特に十字カーソル関連の要素を削除し、チャートスクロールを再度有効化します。その後、操作ガイドを更新し、パネルを再描画します。最後に、不要なオブジェクトを削除するためのクリーンアップ処理を追加します。DeleteMeasureObjects関数では、固定された十字カーソル、計測ライン、およびラベルを削除します。また、DeleteCrosshair関数では、移動する十字カーソルを削除した後、DeleteMeasureObjectsを呼び出して計測関連オブジェクトも削除し、チャートを再描画します。次に、チャートオブジェクトの処理と生成機能を実装していきます。
//+------------------------------------------------------------------+ //| Handle chart drawing for tools | //+------------------------------------------------------------------+ void HandleDrawing(int mx, int my) { datetime time; //--- Declare time double price; //--- Declare price int sw; //--- Declare subwindow if (!ChartXYToTimePrice(local_chart_id, mx, my, sw, time, price)) return; //--- Convert XY to time/price or return string name = "Tool_" + TimeToString(TimeCurrent(), TIME_SECONDS); //--- Set object name color col = clrBlue; //--- Set color ENUM_OBJECT obj_type = OBJ_VLINE; //--- Initialize object type datetime time1 = time; //--- Set time1 double price1 = price; //--- Set price1 datetime time2 = 0; //--- Set time2 double price2 = 0.0; //--- Set price2 bool create = true; //--- Set create flag bool valid_obj = false; //--- Set valid object flag string tool_name = GetToolName(active_tool); //--- Get tool name if (IsSingleClickTool(active_tool)) { //--- Check if single-click tool switch (active_tool) { //--- Switch on tool case TOOL_HLINE: //--- Handle horizontal line obj_type = OBJ_HLINE; //--- Set type time1 = 0; //--- Set time1 price1 = price; //--- Set price1 time2 = 0; //--- Set time2 price2 = 0; //--- Set price2 valid_obj = true; //--- Set valid break; //--- Break case TOOL_VLINE: //--- Handle vertical line obj_type = OBJ_VLINE; //--- Set type time1 = time; //--- Set time1 price1 = 0; //--- Set price1 time2 = 0; //--- Set time2 price2 = 0; //--- Set price2 valid_obj = true; //--- Set valid break; //--- Break case TOOL_TEXT: //--- Handle text obj_type = OBJ_TEXT; //--- Set type time2 = 0; //--- Set time2 price2 = 0; //--- Set price2 valid_obj = true; //--- Set valid break; //--- Break case TOOL_ARROW: //--- Handle arrow obj_type = OBJ_ARROW; //--- Set type time2 = 0; //--- Set time2 price2 = 0; //--- Set price2 valid_obj = true; //--- Set valid break; //--- Break } } else { //--- Handle two-click tools if (!drawing_first_click) { //--- Check if first click draw_time1 = time; //--- Set draw time1 draw_price1 = price; //--- Set draw price1 drawing_first_click = true; //--- Set first click flag create = false; //--- Set no create current_instruction = "Click second point for " + tool_name + "."; //--- Set instruction } else { //--- Handle second click ChartXYToTimePrice(local_chart_id, mx, my, sw, time2, price2); //--- Get time2/price2 drawing_first_click = false; //--- Reset first click time1 = draw_time1; //--- Set time1 price1 = draw_price1; //--- Set price1 current_instruction = "Click on chart to draw."; //--- Set instruction } switch (active_tool) { //--- Switch on tool case TOOL_TRENDLINE: obj_type = OBJ_TREND; valid_obj = true; break; //--- Set trendline case TOOL_RECTANGLE: obj_type = OBJ_RECTANGLE; valid_obj = true; break; //--- Set rectangle case TOOL_FIBO: obj_type = OBJ_FIBO; valid_obj = true; break; //--- Set fibo } } if (create && valid_obj) { //--- Check if create and valid if (ObjectCreate(local_chart_id, name, obj_type, sw, time1, price1, time2, price2)) { //--- Create object ObjectSetInteger(local_chart_id, name, OBJPROP_COLOR, col); //--- Set color ObjectSetInteger(local_chart_id, name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style ObjectSetInteger(local_chart_id, name, OBJPROP_WIDTH, 1); //--- Set width if (obj_type == OBJ_TEXT) ObjectSetString(local_chart_id, name, OBJPROP_TEXT, "Text"); //--- Set text if (obj_type == OBJ_ARROW) ObjectSetInteger(local_chart_id, name, OBJPROP_ARROWCODE, 233); //--- Set arrow code if (obj_type == OBJ_HLINE || obj_type == OBJ_VLINE) ObjectSetInteger(local_chart_id, name, OBJPROP_STYLE, STYLE_DASH); //--- Set dash for lines ObjectSetInteger(local_chart_id, name, OBJPROP_SELECTABLE, true); //--- Set selectable ObjectSetInteger(local_chart_id, name, OBJPROP_SELECTED, true); //--- Set selected ChartRedraw(local_chart_id); //--- Redraw chart active_tool = TOOL_NONE; //--- Deactivate tool current_instruction = ""; //--- Clear instruction } } DrawToolsPanel(); //--- Redraw tools panel }
チャート描画処理を実装するために、アクティブなツールとマウス位置に基づいてチャートオブジェクトの作成を管理するHandleDrawing関数を実装します。まず、ChartXYToTimePriceを使用して、マウス座標を時間と価格の値へ変換します。変換に失敗した場合は、それ以降の処理を行わずに関数を終了します。次に、現在時刻を利用して一意のオブジェクト名を生成します。また、デフォルトの色として青色を設定し、垂直ラインのオブジェクトタイプを初期値として指定します。その後、IsSingleClickToolを使用して1クリックで配置可能なツールかどうかを判定します。1クリックツールの場合は、switch文によって各ツールに応じた設定を行います。水平線ではOBJ_HLINEを設定し、時間値をリセットします。垂直線ではOBJ_VLINEを設定し、価格値をリセットします。テキストではOBJ_TEXT、矢印ではOBJ_ARROWを設定し、オブジェクト作成が有効であることを示すフラグを設定します。
2クリックで描画するツールの場合、まずdrawingfirstclickがfalseであれば1回目のクリックとして始点を保存し、操作ガイドを更新するだけで、オブジェクトは作成しません。 2回目のクリックでは、終点の座標を取得し、drawing_first_clickをリセットします。その後、ツールに応じてオブジェクトタイプを設定します。トレンドラインではOBJ_TREND、矩形ではOBJ_RECTANGLE、フィボナッチではOBJ_FIBOを使用し、作成可能なオブジェクトであることを確認します。作成フラグが有効で、かつオブジェクトが有効な場合は、ObjectCreateを使用してチャートオブジェクトを生成します。その後、色、スタイル、幅、テキスト、または必要に応じて矢印コードなどのプロパティを設定します。ラインの場合は破線設定も適用し、選択可能状態および選択状態を設定します。最後に、ChartRedrawでチャートを更新し、ツールを無効化して操作ガイドをクリアします。そして、DrawToolsPanelを呼び出してツールパネルを更新し、変更内容を反映します。次に、十字カーソルの処理を実装します。それ以外の描画ツールについては、基本的な処理は同様です。
//+------------------------------------------------------------------+ //| Update moving crosshair | //+------------------------------------------------------------------+ void UpdateMovingCrosshair(datetime time, double price) { string h_name = "Cross_H"; //--- Set horizontal name string v_name = "Cross_V"; //--- Set vertical name if (ObjectFind(local_chart_id, h_name) < 0) { //--- Check if horizontal exists ObjectCreate(local_chart_id, h_name, OBJ_HLINE, 0, 0, price); //--- Create horizontal line ObjectSetInteger(local_chart_id, h_name, OBJPROP_COLOR, clrRed); //--- Set color ObjectSetInteger(local_chart_id, h_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style } else { //--- Handle existing ObjectMove(local_chart_id, h_name, 0, 0, price); //--- Move horizontal } if (ObjectFind(local_chart_id, v_name) < 0) { //--- Check if vertical exists ObjectCreate(local_chart_id, v_name, OBJ_VLINE, 0, time, 0); //--- Create vertical line ObjectSetInteger(local_chart_id, v_name, OBJPROP_COLOR, clrRed); //--- Set color ObjectSetInteger(local_chart_id, v_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style } else { //--- Handle existing ObjectMove(local_chart_id, v_name, 0, time, 0); //--- Move vertical } } //+------------------------------------------------------------------+ //| Update fixed crosshair | //+------------------------------------------------------------------+ void UpdateFixedCrosshair() { string hf_name = "Cross_HF"; //--- Set fixed horizontal name string vf_name = "Cross_VF"; //--- Set fixed vertical name if (ObjectFind(local_chart_id, hf_name) < 0) { //--- Check if fixed horizontal exists ObjectCreate(local_chart_id, hf_name, OBJ_HLINE, 0, 0, fixed_price); //--- Create fixed horizontal ObjectSetInteger(local_chart_id, hf_name, OBJPROP_COLOR, clrGreen); //--- Set color ObjectSetInteger(local_chart_id, hf_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style } else { //--- Handle existing ObjectMove(local_chart_id, hf_name, 0, 0, fixed_price); //--- Move fixed horizontal } if (ObjectFind(local_chart_id, vf_name) < 0) { //--- Check if fixed vertical exists ObjectCreate(local_chart_id, vf_name, OBJ_VLINE, 0, fixed_time, 0); //--- Create fixed vertical ObjectSetInteger(local_chart_id, vf_name, OBJPROP_COLOR, clrGreen); //--- Set color ObjectSetInteger(local_chart_id, vf_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style } else { //--- Handle existing ObjectMove(local_chart_id, vf_name, 0, fixed_time, 0); //--- Move fixed vertical } }
UpdateMovingCrosshair関数を実装し、チャート上で動的に表示される十字カーソルを管理します。まず、水平ライン用にCross_H、垂直ライン用にCross_Vというオブジェクト名を定義します。それぞれのオブジェクトが存在するかをObjectFindで確認し、存在しない場合は負の値が返されるため、その場合は作成処理を行います。オブジェクトが存在しない場合は、ObjectCreateを使用して水平ラインにはOBJ_HLINE、垂直ラインにはOBJ_VLINEを作成します。作成時には赤色の実線スタイルを設定します。一方、すでに存在する場合は、ObjectMoveを使用して現在の時間と価格位置へ移動させます。これにより、マウスカーソルの動きに合わせたリアルタイムな追従表示を実現します。次に、固定基準用の十字カーソルを管理するためにUpdateFixedCrosshair関数を作成します。この関数では、Cross_HFとCross_VFというオブジェクト名を使用し、同様にオブジェクトの存在を確認します。存在しない場合は、ObjectCreateによって固定位置用の水平ラインと垂直ラインを作成し、fixed_timeとfixed_priceの位置に配置します。このとき、緑色の実線スタイルを設定します。すでにオブジェクトが存在する場合は、その位置を更新します。これにより、計測モード中でも固定された基準点を保持でき、現在のカーソル位置との比較が可能になります。続いて、十字カーソルのクリック処理を実装します。
//+------------------------------------------------------------------+ //| Handle crosshair click | //+------------------------------------------------------------------+ void HandleCrosshairClick(datetime time, double price) { ulong now = GetMicrosecondCount(); //--- Get current microseconds if (now - last_click_time < 500000) { //--- Check for double-click if (!measuring) { //--- Check not measuring fixed_time = time; //--- Set fixed time fixed_price = price; //--- Set fixed price measuring = true; //--- Set measuring flag ChartSetInteger(0, CHART_MOUSE_SCROLL, false); //--- Disable scroll current_instruction = "Measuring. Double-click to exit."; //--- Set instruction } else { //--- Handle exit measuring measuring = false; //--- Reset measuring DeleteMeasureObjects(); //--- Delete objects ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enable scroll current_instruction = "Move mouse for crosshair. Double-click to start measuring."; //--- Set instruction } DrawToolsPanel(); //--- Redraw tools panel last_click_time = 0; //--- Reset click time } else { //--- Handle single click last_click_time = now; //--- Update click time } }
HandleCrosshairClick関数を実装し、十字カーソルモード中のクリック処理を管理します。まず、GetMicrosecondCountを使用して現在のマイクロ秒単位の時間を取得し、last_click_timeとの差分から500ミリ秒以内のダブルクリックであるかを判定します。ダブルクリックの場合は、計測状態を切り替えます。計測が開始されていない場合は、現在の時間と価格を固定ポイントとして保存し、計測モードを有効化します。また、ChartSetIntegerを使用してチャートのスクロール機能を無効化し、操作ガイドを更新します。すでに計測モードが有効な場合は、計測状態をリセットし、DeleteMeasureObjectsを呼び出して関連オブジェクトを削除します。その後、チャートスクロールを再度有効化し、デフォルトの操作ガイドへ戻します。処理の最後に、ツールパネルを再描画して状態を更新し、次回のクリック判定に備えてクリック時間をリセットします。シングルクリックの場合は、last_click_timeを現在時刻へ更新します。これにより、次のクリックが発生した際にダブルクリックとして判定できるようになります。次に、2つの十字カーソル間を結ぶ計測ラインを更新するためのロジックを実装します。
//+------------------------------------------------------------------+ //| Update measure line | //+------------------------------------------------------------------+ void UpdateMeasureLine(datetime time, double price) { string line_name = "Measure_Line"; //--- Set measure line name if (ObjectFind(local_chart_id, line_name) < 0) { //--- Check if line exists ObjectCreate(local_chart_id, line_name, OBJ_TREND, 0, fixed_time, fixed_price, time, price); //--- Create trend line ObjectSetInteger(local_chart_id, line_name, OBJPROP_COLOR, clrBlue); //--- Set color ObjectSetInteger(local_chart_id, line_name, OBJPROP_STYLE, STYLE_DASHDOT); //--- Set style ObjectSetInteger(local_chart_id, line_name, OBJPROP_WIDTH, 1); //--- Set width ObjectSetInteger(local_chart_id, line_name, OBJPROP_RAY, false); //--- Disable ray } else { //--- Handle existing ObjectMove(local_chart_id, line_name, 0, fixed_time, fixed_price); //--- Move point 0 ObjectMove(local_chart_id, line_name, 1, time, price); //--- Move point 1 } } //+------------------------------------------------------------------+ //| Update measure label | //+------------------------------------------------------------------+ void UpdateMeasureLabel(int mx, int my, datetime time, double price) { string label_name = "Measure_Label"; //--- Set measure label name long period_sec = PeriodSeconds(_Period); //--- Get period seconds long virtual_fixed = fixed_time / period_sec; //--- Compute virtual fixed long virtual_time = time / period_sec; //--- Compute virtual time int bars_diff = (int)MathAbs(virtual_fixed - virtual_time); //--- Compute bars difference double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); //--- Get point value long digits = SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); //--- Get digits double pip_size = (digits == 3 || digits == 5) ? point * 10 : point; //--- Compute pip size double pips = MathAbs(price - fixed_price) / pip_size; //--- Compute pips string price_diff = DoubleToString(MathAbs(price - fixed_price), (int)digits); //--- Format price diff string text = StringFormat("%d bars, %.1f pips, Price Diff: %s", bars_diff, pips, price_diff); //--- Format text if (ObjectFind(local_chart_id, label_name) < 0) { //--- Check if label exists ObjectCreate(local_chart_id, label_name, OBJ_LABEL, 0, 0, 0); //--- Create label ObjectSetInteger(local_chart_id, label_name, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner ObjectSetInteger(local_chart_id, label_name, OBJPROP_XDISTANCE, mx + 20); //--- Set X distance ObjectSetInteger(local_chart_id, label_name, OBJPROP_YDISTANCE, my); //--- Set Y distance ObjectSetString(local_chart_id, label_name, OBJPROP_TEXT, text); //--- Set text ObjectSetInteger(local_chart_id, label_name, OBJPROP_FONTSIZE, 9); //--- Set font size ObjectSetString(local_chart_id, label_name, OBJPROP_FONT, "Arial"); //--- Set font ObjectSetInteger(local_chart_id, label_name, OBJPROP_COLOR, clrBlue); //--- Set color } else { //--- Handle existing ObjectSetInteger(local_chart_id, label_name, OBJPROP_XDISTANCE, mx + 20); //--- Update X distance ObjectSetInteger(local_chart_id, label_name, OBJPROP_YDISTANCE, my); //--- Update Y distance ObjectSetString(local_chart_id, label_name, OBJPROP_TEXT, text); //--- Update text } }
計測ラインを処理するために、計測モード中に2つのポイントを結ぶラインを表示するUpdateMeasureLine関数を実装します。まず、ライン用のオブジェクト名としてMeasure_Lineを定義し、ObjectFindを使用して既存オブジェクトの有無を確認します。オブジェクトが存在しない場合は、ObjectCreateを使用してOBJ_TRENDとして作成します。このラインは固定ポイントから現在の時間・価格位置まで描画され、青色の一点鎖線、幅1に設定し、延長線は無効化します。すでにオブジェクトが存在する場合は、ObjectMoveを使用して始点と終点の位置を更新します。これにより、計測中の2つのポイント間を視覚的につなぐ動的なライン表示を実現します。次に、マウス位置付近に計測値を表示するためにUpdateMeasureLabel関数を作成します。この関数では、まずPeriodSecondsから取得した時間足の秒数を使用して時間差をバー数へ変換します。時間差を秒数で割ることで仮想的なバー数を算出し、MathAbsによって絶対値を取得します。続いて、SymbolInfoDoubleから取得した銘柄のポイント値を基に、価格差をpipsへ変換します。3桁または5桁の価格精度を持つ銘柄については10倍することで調整します。また、価格差は絶対値として計算し、SymbolInfoIntegerから取得した桁数に合わせてフォーマットします。
その後、StringFormatを使用して、バー数、pips値(小数点以下1桁に丸める)、および価格差を含む表示テキストを作成します。ラベルオブジェクトが存在しない場合は、OBJ_LABELとして作成し、左上を基準にマウス位置からオフセットした場所へ配置します。フォントにはArial、サイズ9を設定し、色は青色にします。すでに存在する場合は、位置とテキスト内容を更新します。このラベル計算は、異なる銘柄の価格精度や時間足に対応しながら、時間幅と価格幅を正確に数値化するために重要です。これにより、手動計算を行うことなく、より精密な分析が可能になります。最後に、ツールパレットを実際に操作可能な状態にするため、イベントハンドラ内でチャートイベント処理を実装します。
//+------------------------------------------------------------------+ //| Chart event handler | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if (id == CHARTEVENT_KEYDOWN) { //--- Check key down event if (lparam == 27) { //--- Check escape key DeactivateTool(); //--- Deactivate tool } return; //--- Return } if (id != CHARTEVENT_MOUSE_MOVE) return; //--- Return if not mouse move int mx = (int)lparam; //--- Set mouse X int my = (int)dparam; //--- Set mouse Y int ms = (int)sparam; //--- Set mouse state bool prev_hh = header_hovered; //--- Store previous header hover bool prev_th = theme_hovered; //--- Store previous theme hover bool prev_mh = minimize_hovered; //--- Store previous minimize hover bool prev_ch = close_hovered; //--- Store previous close hover bool prev_rh = resize_hovered; //--- Store previous resize hover header_hovered = IsMouseOverHeader(mx, my); //--- Update header hover theme_hovered = IsMouseOverButton(mx, my, theme_x_offset); //--- Update theme hover minimize_hovered = IsMouseOverButton(mx, my, minimize_x_offset); //--- Update minimize hover close_hovered = IsMouseOverButton(mx, my, close_x_offset); //--- Update close hover resize_hovered = IsMouseOverResize(mx, my, hover_mode); //--- Update resize hover string resize_tooltip = ""; //--- Initialize resize tooltip if (resize_hovered) { //--- Check resize hovered switch (hover_mode) { //--- Switch on hover mode case BOTTOM: resize_tooltip = "Resize Bottom"; break; //--- Set bottom tooltip case RIGHT: resize_tooltip = "Resize Right"; break; //--- Set right tooltip case BOTTOM_RIGHT: resize_tooltip = "Resize Bottom-Right"; break; //--- Set bottom-right tooltip } ObjectSetString(0, canvasToolsName, OBJPROP_TOOLTIP, resize_tooltip); //--- Set tooltip } else if (!panel_minimized && hovered_tool_index >= 0) { //--- Check tool hovered ObjectSetString(0, canvasToolsName, OBJPROP_TOOLTIP, buttons[hovered_tool_index].tooltip); //--- Set tool tooltip } else { //--- Handle no hover ObjectSetString(0, canvasToolsName, OBJPROP_TOOLTIP, ""); //--- Clear tooltip } if (prev_hh != header_hovered || prev_th != theme_hovered || prev_mh != minimize_hovered || prev_ch != close_hovered || prev_rh != resize_hovered) { //--- Check if hovers changed DrawHeader(); //--- Redraw header if (!panel_minimized) DrawToolsPanel(); //--- Redraw tools if not minimized ChartRedraw(); //--- Redraw chart } if (!panel_minimized) { //--- Check not minimized int ht = GetHoveredTool(mx, my); //--- Get hovered tool if (ht != hovered_tool_index) { //--- Check if changed hovered_tool_index = ht; //--- Update index DrawToolsPanel(); //--- Redraw tools ChartRedraw(); //--- Redraw chart } } if (resize_hovered || resizing) { //--- Check resize hovered or resizing hover_mouse_local_x = mx - currentCanvasX; //--- Update local X hover_mouse_local_y = my - (currentCanvasY + header_height + tools_y_offset); //--- Update local Y DrawToolsPanel(); //--- Redraw tools ChartRedraw(); //--- Redraw chart } if (ms == 1 && prev_mouse_state == 0) { //--- Check mouse down if (header_hovered) { //--- Check header hovered panel_dragging = true; //--- Set dragging flag panel_drag_x = mx; //--- Set drag X panel_drag_y = my; //--- 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 DrawHeader(); //--- Redraw header ChartRedraw(); //--- Redraw chart } else if (theme_hovered) { //--- Check theme hovered ToggleTheme(); //--- Toggle theme } else if (minimize_hovered) { //--- Check minimize hovered ToggleMinimize(); //--- Toggle minimize } else if (close_hovered) { //--- Check close hovered OnDeinit(0); //--- Call deinit } else if (!panel_minimized) { //--- Check not minimized int ht = GetHoveredTool(mx, my); //--- Get hovered tool if (ht >= 0) { //--- Check valid tool ToggleTool(buttons[ht].type); //--- Toggle tool } else if (active_tool != TOOL_NONE && active_tool != TOOL_CROSSHAIR) { //--- Check active tool HandleDrawing(mx, my); //--- Handle drawing } else if (IsMouseOverResize(mx, my, resize_mode)) { //--- Check resize area resizing = true; //--- Set resizing flag resize_start_x = mx; //--- Set start X resize_start_y = my; //--- Set start Y start_width = currentWidth; //--- Set start width start_height = currentHeight; //--- Set start height ChartSetInteger(0, CHART_MOUSE_SCROLL, false); //--- Disable scroll DrawToolsPanel(); //--- Redraw tools ChartRedraw(); //--- Redraw chart } } } if (panel_dragging && ms == 1) { //--- Check dragging int dx = mx - panel_drag_x; //--- Compute delta X int dy = my - panel_drag_y; //--- Compute delta Y currentCanvasX = MathMax(0, MathMin((int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS) - currentWidth, panel_start_x + dx)); //--- Update X currentCanvasY = MathMax(0, MathMin((int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS) - header_height - (panel_minimized ? 0 : currentHeight) - tools_y_offset, panel_start_y + dy)); //--- Update Y ObjectSetInteger(0, canvasHeaderName, OBJPROP_XDISTANCE, currentCanvasX); //--- Set header X ObjectSetInteger(0, canvasHeaderName, OBJPROP_YDISTANCE, currentCanvasY); //--- Set header Y if (!panel_minimized) { //--- Check not minimized ObjectSetInteger(0, canvasToolsName, OBJPROP_XDISTANCE, currentCanvasX); //--- Set tools X ObjectSetInteger(0, canvasToolsName, OBJPROP_YDISTANCE, currentCanvasY + header_height + tools_y_offset); //--- Set tools Y } ChartRedraw(); //--- Redraw chart } if (resizing && ms == 1) { //--- Check resizing int dx = mx - resize_start_x; //--- Compute delta X int dy = my - resize_start_y; //--- Compute delta Y int nw = MathMax(min_width, start_width + (resize_mode == RIGHT || resize_mode == BOTTOM_RIGHT ? dx : 0)); //--- Compute new width int nh = MathMax(min_height, start_height + (resize_mode == BOTTOM || resize_mode == BOTTOM_RIGHT ? dy : 0)); //--- Compute new height nw = MathMin(nw, (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS) - currentCanvasX); //--- Clamp width nh = MathMin(nh, (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS) - currentCanvasY - header_height - tools_y_offset); //--- Clamp height currentWidth = nw; //--- Update width currentHeight = nh; //--- Update height canvasTools.Resize(currentWidth, currentHeight); //--- Resize tools canvas ObjectSetInteger(0, canvasToolsName, OBJPROP_XSIZE, currentWidth); //--- Set tools X size ObjectSetInteger(0, canvasToolsName, OBJPROP_YSIZE, currentHeight); //--- Set tools Y size canvasHeader.Resize(currentWidth, header_height); //--- Resize header canvas ObjectSetInteger(0, canvasHeaderName, OBJPROP_XSIZE, currentWidth); //--- Set header X size ObjectSetInteger(0, canvasHeaderName, OBJPROP_YSIZE, header_height); //--- Set header Y size DrawHeader(); //--- Redraw header DrawToolsPanel(); //--- Redraw tools ChartRedraw(); //--- Redraw chart } if (ms == 0 && prev_mouse_state == 1) { //--- Check mouse up panel_dragging = false; //--- Reset dragging resizing = false; //--- Reset resizing ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enable scroll DrawHeader(); //--- Redraw header DrawToolsPanel(); //--- Redraw tools ChartRedraw(); //--- Redraw chart } datetime time = 0; //--- Initialize time double price = 0.0; //--- Initialize price int sw = 0; //--- Initialize subwindow if (ChartXYToTimePrice(local_chart_id, mx, my, sw, time, price)) { //--- Convert XY to time/price if (active_tool == TOOL_CROSSHAIR) { //--- Check crosshair active UpdateMovingCrosshair(time, price); //--- Update moving crosshair if (measuring) { //--- Check measuring UpdateFixedCrosshair(); //--- Update fixed crosshair UpdateMeasureLine(time, price); //--- Update measure line UpdateMeasureLabel(mx, my, time, price); //--- Update measure label } ChartRedraw(); //--- Redraw chart } if (ms == 1 && prev_mouse_state == 0 && active_tool == TOOL_CROSSHAIR && !header_hovered && !theme_hovered && !minimize_hovered && !close_hovered && !resize_hovered && (panel_minimized || GetHoveredTool(mx, my) == -1)) { //--- Check crosshair click conditions HandleCrosshairClick(time, price); //--- Handle click ChartRedraw(); //--- Redraw chart } } prev_mouse_state = ms; //--- Update previous state }
OnChartEventイベントハンドラーを使用して、パレットおよびツールに対するすべてのユーザー操作を管理します。まず、CHARTEVENT_KEYDOWNを確認して、エスケープキー(lparamが27)を検出します。検出した場合はDeactivateToolを使用して現在のツールを無効化し、その後早期リターンします。参考までに、エスケープ文字はASCIIテーブルではコード27として定義されています。以下をご覧ください。

次に、イベントがCHARTEVENT_MOUSE_MOVEでない場合は処理を終了します。それ以外の場合は、パラメータからマウスのX座標、Y座標、状態を取得し、以前のホバー状態フラグを保存します。その後、IsMouseOverHeader、テーマ/最小化/閉じるボタン用のIsMouseOverButton、およびhover_modeを設定するIsMouseOverResizeなどの関数を使用して、現在のホバー状態を更新します。続いて、ObjectSetStringを使用してツールキャンバス上のツールチップを設定します。リサイズモードまたはホバー中のツールボタンがある場合は対応するツールチップを表示し、対象がない場合はクリアします。いずれかのホバー状態に変更があった場合は、ヘッダーを再描画し、最小化されていなければツールパネルも再描画した後、ChartRedraw関数を呼び出します。最小化されていない場合は、GetHoveredToolを使用してホバー中のツールインデックスを検出し、変更があれば再描画します。リサイズ状態の場合は、ローカルのホバー座標を更新し、ツールパネルを再描画します。
マウスボタン押下時(msが1、prevが0の場合)は、ヘッダー上にマウスがある場合、フラグを設定してドラッグを開始し、スクロールを無効化してヘッダーを再描画します。テーマボタンまたは最小化ボタン上にある場合は、それぞれの状態を切り替えます。閉じるボタン上にある場合はdeinitを呼び出します。ツールボタン上にある場合は、そのツールタイプを切り替えます。十字カーソル以外の描画処理についてはHandleDrawingで処理し、リサイズ領域上にある場合はフラグ、開始位置などを設定してリサイズを開始し、スクロールを無効化した後、ツールパネルを再描画します。ドラッグ中(msが1の場合)は、移動量を計算し、MathMaxおよびMathMin関数を使用して新しい位置をチャート範囲内に制限します。その後、グローバル変数を更新し、ヘッダーおよびツールパネル(表示中の場合)のオブジェクト距離を設定して、チャートを再描画します。リサイズ中(msが1の場合)は、モードに基づいて新しいサイズを計算し、範囲内に制限します。グローバル変数を更新し、Resizeでキャンバスサイズを変更した後、ObjectSetIntegerでサイズを設定します。その後、ヘッダー、ツールパネル、およびチャートを再描画します。
マウスボタン解放時(msが0、prevが1の場合)は、ドラッグ状態およびリサイズ状態のフラグをリセットし、スクロールを再有効化します。その後、ヘッダーとツールパネルを再描画し、チャートを更新します。ChartXYToTimePriceを使用してマウス座標を時間・価格座標へ変換します。十字カーソルが有効な場合は移動中の十字カーソルを更新し、計測中の場合は固定位置、計測ライン、およびラベルも更新します。条件を満たすダウンクリックがパレット外で発生した場合は、HandleCrosshairClickで処理し、再描画を実行します。最後に、次回イベント処理のためにprev_mouse_stateを現在の状態へ更新し、状態を保持した応答性の高いインタラクションを実現します。最後に、不要になったオブジェクトを削除します。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { canvasHeader.Destroy(); //--- Destroy header canvas canvasTools.Destroy(); //--- Destroy tools canvas DeleteCrosshair(); //--- Delete crosshair measuring = false; //--- Reset measuring flag ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enable mouse scroll int total = ObjectsTotal(local_chart_id); //--- Get total objects for (int i = total - 1; i >= 0; i--) { //--- Loop over objects backward string obj_name = ObjectName(local_chart_id, i); //--- Get object name if (StringFind(obj_name, "Tool_") == 0) { //--- Check if our tool ObjectDelete(local_chart_id, obj_name); //--- Delete object } } ChartRedraw(); //--- Redraw chart }
OnDeinitイベントハンドラでは、Destroyメソッドを使用してヘッダーおよびツールキャンバスを破棄し、DeleteCrosshairを使用して有効になっている十字カーソルを削除します。その後、計測フラグをfalseにリセットし、ChartSetInteger関数を使用してマウススクロールを再有効化します。不要なオブジェクトが残らないようにするため、ローカルチャート上のオブジェクト総数をObjectsTotalで取得します。その後、安全に削除できるように後ろからループ処理を行い、ObjectNameを使用してオブジェクト名を取得します。そして、名前が「Tool_」で始まるオブジェクトをObjectDelete関数で削除します。最後に、ChartRedrawを呼び出してチャートを更新し、プログラム終了時のリソース解放処理を完了します。コンパイルすると、次の結果が得られます。

表示結果から、インタラクティブなツールパレットを作成できたことが確認でき、設定した目的を達成しています。残る作業はシステムの動作検証であり、これは次のセクションで扱います。
バックテスト
テストを実施し、以下はコンパイル後のビジュアライゼーションを単一のGraphics Interchange Format (GIF)画像としてまとめたビジュアライゼーションです。

結論
MQL5でチャート描画用のインタラクティブなツールパレットを構築しました。このパレットには、ドラッグおよびリサイズ可能なパネル、さらにダークモードとライトモードを切り替えるテーマ機能が搭載されています。十字カーソル、トレンドライン、ライン、矩形、フィボナッチリトレースメント、テキスト、矢印など、さまざまなツール用のボタンを組み込み、マウスイベントを管理することで、スムーズなツールの有効化とチャート上での操作を実現しました。このカスタマイズ可能なシステムにより、リアルタイムのフィードバックや操作説明を提供し、効率的なワークフローを実現する柔軟なUIを構築できます。このインタラクティブなツールパレットにより、チャートへの注釈追加や測定作業を効率化でき、高度なトレード戦略への統合に向けた準備が整いました。以降のパートでは、より整理されたツール構成を備えた、さらに詳細なパレットへ拡張していきます。どうぞお楽しみに。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/21275
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
MQL5入門(第41回)MQL5におけるファイル処理入門(III)
MQL5入門(第40回):MQL5におけるファイル処理入門(II)
MQL5でカスタムインジケータを作成する(第7回):セッション分析のためのハイブリッドTime Price Opportunity (TPO)マーケットプロファイル
Market Memory Zonesインジケーターの自動化:価格が戻りやすい領域
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索