English Deutsch
preview
MQL5取引ツール(第8回):ドラッグ&最小化可能な拡張情報ダッシュボード

MQL5取引ツール(第8回):ドラッグ&最小化可能な拡張情報ダッシュボード

MetaTrader 5トレーディング |
17 0
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回の記事(第7回)では、MetaQuotes Language 5 (MQL5)で、複数銘柄のポジションや口座指標(「残高」「証拠金」「余剰証拠金」など)を監視できる情報ダッシュボードを開発しました。このダッシュボードには、列のソート機能やComma Separated Values (CSV)形式でのエクスポート機能も実装しました。今回の第8回では、このダッシュボードをさらに拡張し、ドラッグ&最小化機能を追加するほか、ポジションのクローズや表示切り替え、エクスポートのためのインタラクティブボタンや、ホバーエフェクトを導入し、より動的なユーザー体験を実現します。この拡張により、リアルタイムのポジション追跡やヘッダーのグロー効果は維持されます。本記事では以下のトピックを扱います。

  1. 拡張ダッシュボードアーキテクチャの理解
  2. MQL5での実装
  3. バックテスト
  4. 結論

これらを通じて、効率的な取引監視に適した、汎用性が高く使いやすいMQL5ダッシュボードを作成できるようになります。それでは、さっそく始めましょう。


拡張ダッシュボードアーキテクチャの理解

第7回の情報ダッシュボードをベースに、ドラッグ&最小化機能、インタラクティブボタン、ホバーエフェクトを追加することで、複数ポジションの管理をより柔軟かつユーザーフレンドリーにします。これらの拡張は、ダッシュボードをチャート上の任意の位置に移動できるようにして分析時の画面の混雑を減らすことや、最小化機能で画面スペースを節約すること、さらにインタラクティブ要素で即時の視覚的フィードバックを提供することに有効です。これにより、スピードが求められる取引環境でも快適に操作できます。

具体的には、ドラッグ操作やボタンクリックのためのマウスイベント処理を組み込み、コアとなるポジション追跡機能を失うことなく、ダッシュボードをレスポンシブで適応性のあるものにします。また、エクスポート機能はヘッダーにアイコンを追加して視認性を高めつつ、従来のキーボードショートカットも維持します。これらの拡張を加えながら、ソート可能なグリッドやリアルタイム更新を維持することで、日常の取引で直感的かつ効率的に使えるツールを作り上げます。以下では、目指す機能の概要を示したうえで、実装の説明に進みます。

実施計画


MQL5での実装

MQL5でプログラムを拡張するために、新しいダッシュボードコンポーネントを定義する必要があります。通常、これらは4つのコンポーネントになります。

//--- existing components

#define HEADER_PANEL_TEXT   "HEADER_PANEL_TEXT"//--- Header title label
#define CLOSE_BUTTON        "BUTTON_CLOSE"     //--- Close button identifier
#define EXPORT_BUTTON       "BUTTON_EXPORT"    //--- Export button identifier
#define TOGGLE_BUTTON       "BUTTON_TOGGLE"    //--- Toggle (minimize/maximize) button

//--- the rest of the components

既存のコンポーネントに加えて、情報ダッシュボードの拡張機能をサポートするための新しい定義を追加します。これにより、インタラクティブなユーザーインターフェイス(UI)要素の識別子を導入できます。HEADER_PANEL_TEXTはダッシュボードのタイトルラベル用にHEADER_PANEL_TEXTと定義しており、明確な視覚的ヘッダーを提供します。CLOSE_BUTTONはBUTTON_CLOSEと定義し、ダッシュボードを閉じるためのボタン識別子を作成します。これにより、チャート上からダッシュボードを削除できます。EXPORT_BUTTONはBUTTON_EXPORTと定義し、CSVエクスポートを実行するボタンをセットアップします。これにより、データのアクセス性が向上します。TOGGLE_BUTTONはBUTTON_TOGGLEと定義し、ダッシュボードを最小化・最大化するボタンを有効にします。これにより、画面スペースの管理が容易になります。

これらの定義により、新しいインタラクティブコンポーネントの命名が整理され、ドラッグ&最小化機能拡張をサポートできます。次におこなう変更としては、ヘッダーのシャドウ色を明確に定義されたMQL5定数に置き換えます。

// Dashboard settings
struct DashboardSettings {                     //--- Structure for dashboard settings
   int      panel_x;                           //--- X-coordinate of panel
   int      panel_y;                           //--- Y-coordinate of panel
   int      row_height;                        //--- Height of each row
   int      font_size;                         //--- Font size for labels
   string   font;                              //--- Font type for labels
   color    bg_color;                          //--- Background color of main panel
   color    border_color;                      //--- Border color of panels
   color    header_color;                      //--- Default color for header text
   color    text_color;                        //--- Default color for text
   color    section_bg_color;                  //--- Background color for header/footer panels
   int      zorder_panel;                      //--- Z-order for main panel
   int      zorder_subpanel;                   //--- Z-order for sub-panels
   int      zorder_labels;                     //--- Z-order for labels
   int      label_y_offset;                    //--- Y-offset for label positioning
   int      label_x_offset;                    //--- X-offset for label positioning
   int      header_x_distances[9];             //--- X-distances for header labels (9 columns)
   color    header_shades[12];                 //--- Array of header color shades for glow effect
} settings = {                                 //--- Initialize settings with default values
   20,                                         //--- panel_x
   20,                                         //--- panel_y
   24,                                         //--- row_height
   11,                                         //--- font_size
   "Calibri Bold",                             //--- font
   C'240,240,240',                             //--- bg_color (light gray)
   clrBlack,                                   //--- border_color
   C'0,50,70',                                 //--- header_color (dark teal)
   clrBlack,                                   //--- text_color
   C'200,220,230',                             //--- section_bg_color (light blue-gray)
   100,                                        //--- zorder_panel
   101,                                        //--- zorder_subpanel
   102,                                        //--- zorder_labels
   3,                                          //--- label_y_offset
   25,                                         //--- label_x_offset
   {10, 120, 170, 220, 280, 330, 400, 470, 530}, //--- header_x_distances
   {clrBlack, clrRed, clrBlue, clrGreen, clrMagenta, clrDarkOrchid,
    clrDeepPink, clrSkyBlue, clrDodgerBlue, clrDarkViolet, clrOrange, clrCrimson} //--- header_shades
};

//--- the previous one was as below
/*
   //---
   {C'0,0,0', C'255,0,0', C'0,255,0', C'0,0,255', C'255,255,0', C'0,255,255', 
    C'255,0,255', C'255,255,255', C'255,0,255', C'0,255,255', C'255,255,0', C'0,0,255'}
*/

ここでは、DashboardSettings構造体を拡張し、header_shades配列を更新してヘッダーのグロー効果を改善し、視覚的に魅力的な表示を実現します。従来、header_shadesには基本的なRGB色(たとえば、純粋な黒、赤、緑、青、黄、シアン、マゼンタ、白)が混在しており、グローサイクルに使用されていました。今回、header_shadesは厳選された12色(clrBlack、clrRed、clrBlue、clrGreen、clrMagenta、clrDarkOrchid、clrDeepPink、clrSkyBlue、clrDodgerBlue、clrDarkViolet、clrOrange、clrCrimson)で定義します。

このアップグレードにより、より豊かで多彩なパレットがグローサイクルに反映され、ヘッダーの強調表示機能を維持しながら、ダッシュボードの美観が向上します。最後に、ホバーやドラッグ状態を管理するための追加のグローバル変数も定義し、ダッシュボードをより動的に操作できるようにします。

//--- added global variables


int          prev_num_symbols = 0;             //--- Previous number of active symbols
bool         panel_is_visible = true;          //--- Flag to control panel visibility
bool         panel_minimized = false;          //--- Flag to control minimized state
bool         panel_dragging = false;           //--- Flag to track if panel is being dragged
int          panel_drag_x = 0;                 //--- Mouse x-coordinate when drag starts
int          panel_drag_y = 0;                 //--- Mouse y-coordinate when drag starts
int          panel_start_x = 0;                //--- Panel x-coordinate when drag starts
int          panel_start_y = 0;                //--- Panel y-coordinate when drag starts
int          prev_mouse_state = 0;             //--- Previous mouse state
bool         header_hovered = false;           //--- Header hover state
bool         toggle_hovered = false;           //--- Toggle button hover state
bool         close_hovered = false;            //--- Close button hover state
bool         export_hovered = false;           //--- Export button hover state
int          last_mouse_x = 0;                 //--- Last mouse x position
int          last_mouse_y = 0;                 //--- Last mouse y position
bool         prev_header_hovered = false;      //--- Previous header hover state
bool         prev_toggle_hovered = false;      //--- Previous toggle hover state
bool         prev_close_hovered = false;       //--- Previous close button hover state
bool         prev_export_hovered = false;      //--- Previous export button hover state

最後に、拡張されたインタラクティビティとドラッグ機能をサポートするために、追加のグローバル変数を導入します。まず、prev_num_symbolsを0に設定し、アクティブな銘柄数の変化を追跡してダッシュボードの動的リサイズに対応します。panel_is_visibleをtrueに設定してダッシュボードの表示/非表示を制御し、panel_minimizedをfalseに設定して最小化状態を管理します。ドラッグ機能を有効にするために、panel_draggingをfalseに設定してドラッグ状態を追跡し、panel_drag_xとpanel_drag_yを0に設定してドラッグ開始時のマウス座標を保持します。さらに、panel_start_xとpanel_start_yを0に設定してドラッグ開始時のパネル座標を保持します。

マウスクリック状態を監視するためにprev_mouse_stateを0に設定します。ホバーエフェクト用には、header_hovered、toggle_hovered、close_hovered、export_hoveredをfalseに設定し、ヘッダーや各ボタンのホバー状態を追跡します。最後のマウス位置はlast_mouse_xとlast_mouse_yに0を設定して保存し、ホバー状態の変化を検出するためにprev_header_hovered、prev_toggle_hovered、prev_close_hovered、prev_export_hoveredをfalseに設定します。

これらの変数により、ドラッグや最小化、ホバー時のフィードバックなど、動的なUI操作が可能になります。変数の更新が完了したので、次に関数も更新してオブジェクト作成を標準化します。これにより、モジュール化された高度な設計を実現できます。まずはラベルを作成し、ツールチップ機能を追加する関数から始めます。

//+------------------------------------------------------------------+
//| Creating label object                                            |
//+------------------------------------------------------------------+
bool createLABEL(string objName, string txt, int xD, int yD, color clrTxt, int fontSize, string font, int anchor, string tooltip = "", bool selectable = false) {
   if(!ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0)) { //--- Creating label object
      Print(__FUNCTION__, ": Failed to create label '", objName, "'. Error code = ", GetLastError()); //--- Logging creation failure
      return(false);                                   //--- Returning failure
   }
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xD);                  //--- Setting x-coordinate
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yD);                  //--- Setting y-coordinate
   ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER);      //--- Setting corner alignment
   ObjectSetString(0, objName, OBJPROP_TEXT, txt);                       //--- Setting text content
   ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize);             //--- Setting font size
   ObjectSetString(0, objName, OBJPROP_FONT, font);                      //--- Setting font type
   ObjectSetInteger(0, objName, OBJPROP_COLOR, clrTxt);                  //--- Setting text color
   ObjectSetInteger(0, objName, OBJPROP_BACK, false);                    //--- Setting to foreground
   ObjectSetInteger(0, objName, OBJPROP_STATE, selectable);              //--- Setting selectable state
   ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, selectable);         //--- Setting selectability
   ObjectSetInteger(0, objName, OBJPROP_SELECTED, false);                //--- Setting not selected
   ObjectSetInteger(0, objName, OBJPROP_ANCHOR, anchor);                 //--- Setting anchor point
   ObjectSetInteger(0, objName, OBJPROP_ZORDER, settings.zorder_labels); //--- Setting z-order


   //--- added this tooltip feature
   ObjectSetString(0, objName, OBJPROP_TOOLTIP, tooltip == "" ? (selectable ? "Click to sort" : "Position data") : tooltip); //--- Setting tooltip text
   //---
   //--- the existing was a hardcoded to this
   // ObjectSetString(0, objName, OBJPROP_TOOLTIP, selectable ? "Click to sort" : "Position data"); //--- Set tooltip
   //---


   ChartRedraw(0);                                    //--- Redrawing chart
   return(true);                                      //--- Returning success
}

createLABEL関数では、ツールチップのロジックを改善し、さまざまなUI要素で柔軟かつ再利用可能にしました。従来は、ツールチップがObjectSetStringOBJPROP_TOOLTIPを設定する際にハードコードされており、選択可能なラベルでは「Click to sort」、選択不可のラベルでは「Position data」と固定されていたため、カスタマイズの幅が限られていました。今回、tooltipパラメータを追加し、デフォルトは空文字に設定しました。ObjectSetStringでOBJPROP_TOOLTIPを設定する際に三項演算子を使用し、tooltipが空の場合は選択可能ラベルでは「Click to sort」、その他では「Position data」をデフォルトとして使用し、tooltipが指定されていればその値を使用するように変更しました。この変更により、「Minimize dashboard」や「Close dashboard」などのボタン固有のツールチップを設定できる一方で、ヘッダーやデータラベルではデフォルトを維持でき、ユーザーへの案内と操作性の明確化が向上します。

次に、パネル作成を標準化するために、OnInitイベントハンドラ内でのインラインのオブジェクト作成呼び出しを関数に置き換え、保守性を向上させます。以下はその際に採用するロジックです。

//+------------------------------------------------------------------+
//| Creating rectangle object                                        |
//+------------------------------------------------------------------+
bool createRectangle(string object_name, int x_distance, int y_distance, int x_size, int y_size,
                     color background_color, color border_color = clrBlack) {
   if(!ObjectCreate(0, object_name, OBJ_RECTANGLE_LABEL, 0, 0, 0)) { //--- Creating rectangle object
      Print(__FUNCTION__, ": Failed to create Rectangle: '", object_name, "'. Error code = ", GetLastError()); //--- Logging creation failure
      return(false);                                  //--- Returning failure
   }
   ObjectSetInteger(0, object_name, OBJPROP_XDISTANCE, x_distance);            //--- Setting x-coordinate
   ObjectSetInteger(0, object_name, OBJPROP_YDISTANCE, y_distance);            //--- Setting y-coordinate
   ObjectSetInteger(0, object_name, OBJPROP_XSIZE, x_size);                    //--- Setting width
   ObjectSetInteger(0, object_name, OBJPROP_YSIZE, y_size);                    //--- Setting height
   ObjectSetInteger(0, object_name, OBJPROP_CORNER, CORNER_LEFT_UPPER);        //--- Setting corner alignment
   ObjectSetInteger(0, object_name, OBJPROP_BGCOLOR, background_color);        //--- Setting background color
   ObjectSetInteger(0, object_name, OBJPROP_BORDER_COLOR, border_color);       //--- Setting border color
   ObjectSetInteger(0, object_name, OBJPROP_BORDER_TYPE, BORDER_FLAT);         //--- Setting border type
   ObjectSetInteger(0, object_name, OBJPROP_BACK, false);                      //--- Setting to foreground
   ObjectSetInteger(0, object_name, OBJPROP_ZORDER, settings.zorder_subpanel); //--- Setting z-order
   return(true);                                                               //--- Returning success
}

ここでは、単純にboolean型のcreateRectangle関数を作成し、ラベル作成時と同様の構造で定義をおこないます。特に新しい内容ではないため、時間を節約して次の更新に進みます。次の更新はカウント関数の軽微な修正で、ループの方向を変更するものです。

//+------------------------------------------------------------------+
//| Counting total positions for a symbol                            |
//+------------------------------------------------------------------+
string countPositionsTotal(string symbol) {
   int totalPositions = 0;                               //--- Initializing position counter
   int count_Total_Pos = PositionsTotal();               //--- Getting total positions
   for(int i = count_Total_Pos - 1; i >= 0; i--) {       //--- Iterating through positions
      ulong ticket = PositionGetTicket(i);               //--- Getting position ticket
      if(ticket > 0 && PositionSelectByTicket(ticket)) { //--- Checking if position selected
         if(PositionGetString(POSITION_SYMBOL) == symbol && (MagicNumber < 0 || PositionGetInteger(POSITION_MAGIC) == MagicNumber)) totalPositions++; //--- Checking symbol and magic
      }
   }
   return IntegerToString(totalPositions);               //--- Returning total as string
}

//+------------------------------------------------------------------+
//| Counting buy or sell positions for a symbol                      |
//+------------------------------------------------------------------+
string countPositions(string symbol, ENUM_POSITION_TYPE pos_type) {
   int totalPositions = 0;                               //--- Initializing position counter
   int count_Total_Pos = PositionsTotal();               //--- Getting total positions
   for(int i = count_Total_Pos - 1; i >= 0; i--) {       //--- Iterating through positions
      ulong ticket = PositionGetTicket(i);               //--- Getting position ticket
      if(ticket > 0 && PositionSelectByTicket(ticket)) { //--- Checking if position selected
         if(PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_TYPE) == pos_type && (MagicNumber < 0 || PositionGetInteger(POSITION_MAGIC) == MagicNumber)) { //--- Checking symbol, type, magic
            totalPositions++;                            //--- Incrementing counter
         }
      }
   }
   return IntegerToString(totalPositions);               //--- Returning total as string
}

//+------------------------------------------------------------------+
//| Counting pending orders for a symbol                             |
//+------------------------------------------------------------------+
string countOrders(string symbol) {
   int total = 0;                                        //--- Initializing counter
   int tot = OrdersTotal();                              //--- Getting total orders
   for(int i = tot - 1; i >= 0; i--) {                   //--- Iterating through orders
      ulong ticket = OrderGetTicket(i);                  //--- Getting order ticket
      if(ticket > 0 && OrderSelect(ticket)) {            //--- Checking if order selected
         if(OrderGetString(ORDER_SYMBOL) == symbol && (MagicNumber < 0 || OrderGetInteger(ORDER_MAGIC) == MagicNumber)) total++; //--- Checking symbol and magic
      }
   }
   return IntegerToString(total);                        //--- Returning total as string
}

//+------------------------------------------------------------------+
//| Summing double property for positions of a symbol                |
//+------------------------------------------------------------------+
string sumPositionDouble(string symbol, ENUM_POSITION_PROPERTY_DOUBLE prop) {
   double total = 0.0;                                   //--- Initializing total
   int count_Total_Pos = PositionsTotal();               //--- Getting total positions
   for(int i = count_Total_Pos - 1; i >= 0; i--) {       //--- Iterating through positions
      ulong ticket = PositionGetTicket(i);               //--- Getting position ticket
      if(ticket > 0 && PositionSelectByTicket(ticket)) { //--- Checking if position selected
         if(PositionGetString(POSITION_SYMBOL) == symbol && (MagicNumber < 0 || PositionGetInteger(POSITION_MAGIC) == MagicNumber)) { //--- Checking symbol and magic
            total += PositionGetDouble(prop);            //--- Adding property value
         }
      }
   }
   return DoubleToString(total, 2);                      //--- Returning total as string
}

//+------------------------------------------------------------------+
//| Summing commission for positions of a symbol from history        |
//+------------------------------------------------------------------+
double sumPositionCommission(string symbol) {
   double total_comm = 0.0;                             //--- Initializing total commission
   int pos_total = PositionsTotal();                    //--- Getting total positions
   for(int p = 0; p < pos_total; p++) {                 //--- Iterating through positions
      ulong ticket = PositionGetTicket(p);              //--- Getting position ticket
      if(ticket > 0 && PositionSelectByTicket(ticket)) { //--- Checking if selected
         if(PositionGetString(POSITION_SYMBOL) == symbol && (MagicNumber < 0 || PositionGetInteger(POSITION_MAGIC) == MagicNumber)) { //--- Checking symbol and magic
            long pos_id = PositionGetInteger(POSITION_IDENTIFIER); //--- Getting position ID
            if(HistorySelectByPosition(pos_id)) {       //--- Selecting history by position
               int deals_total = HistoryDealsTotal();   //--- Getting total deals
               for(int d = 0; d < deals_total; d++) {   //--- Iterating through deals
                  ulong deal_ticket = HistoryDealGetTicket(d); //--- Getting deal ticket
                  if(deal_ticket > 0) {                 //--- Checking valid
                     total_comm += HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION); //--- Adding commission
                  }
               }
            }
         }
      }
   }
   return total_comm;                                  //--- Returning total commission
}  

//+------------------------------------------------------------------+
//| Collecting active symbols with positions or orders               |
//+------------------------------------------------------------------+
void CollectActiveSymbols() {
   string symbols_temp[];                             //--- Temporary array for symbols
   int added = 0;                                     //--- Counter for added symbols
   // Collecting from positions
   int pos_total = PositionsTotal();                  //--- Getting total positions
   for(int i = pos_total - 1; i >= 0; i--) {          //--- Iterating through positions
      ulong ticket = PositionGetTicket(i);            //--- Getting position ticket
      if(ticket > 0 && PositionSelectByTicket(ticket)) { //--- Checking if position selected
         if(MagicNumber < 0 || PositionGetInteger(POSITION_MAGIC) == MagicNumber) { //--- Checking magic number
            string sym = PositionGetString(POSITION_SYMBOL); //--- Getting symbol
            bool found = false;                       //--- Flag for symbol found
            for(int k = 0; k < added; k++) {          //--- Checking existing symbols
               if(symbols_temp[k] == sym) {           //--- Symbol already added
                  found = true;                       //--- Setting found flag
                  break;                              //--- Exiting loop
               }
            }
            if(!found) {                              //--- If not found
               ArrayResize(symbols_temp, added + 1);  //--- Resizing array
               symbols_temp[added] = sym;             //--- Adding symbol
               added++;                               //--- Incrementing counter
            }
         }
      }
   }
   // Collecting from orders
   int ord_total = OrdersTotal();                     //--- Getting total orders
   for(int i = ord_total - 1; i >= 0; i--) {          //--- Iterating through orders
      ulong ticket = OrderGetTicket(i);               //--- Getting order ticket
      if(ticket > 0 && OrderSelect(ticket)) {         //--- Checking if order selected
         if(MagicNumber < 0 || OrderGetInteger(ORDER_MAGIC) == MagicNumber) { //--- Checking magic number
            string sym = OrderGetString(ORDER_SYMBOL); //--- Getting symbol
            bool found = false;                       //--- Flag for symbol found
            for(int k = 0; k < added; k++) {          //--- Checking existing symbols
               if(symbols_temp[k] == sym) {           //--- Symbol already added
                  found = true;                       //--- Setting found flag
                  break;                              //--- Exiting loop
               }
            }
            if(!found) {                             //--- If not found
               ArrayResize(symbols_temp, added + 1); //--- Resizing array
               symbols_temp[added] = sym;            //--- Adding symbol
               added++;                              //--- Incrementing counter
            }
         }
      }
   }
   // Setting symbol_data
   ArrayResize(symbol_data, added);                   //--- Resizing symbol data array
   for(int i = 0; i < added; i++) {                   //--- Iterating through added symbols
      symbol_data[i].name = symbols_temp[i];          //--- Setting symbol name
      symbol_data[i].buys = 0;                        //--- Initializing buys
      symbol_data[i].sells = 0;                       //--- Initializing sells
      symbol_data[i].trades = 0;                      //--- Initializing trades
      symbol_data[i].lots = 0.0;                      //--- Initializing lots
      symbol_data[i].profit = 0.0;                    //--- Initializing profit
      symbol_data[i].pending = 0;                     //--- Initializing pending
      symbol_data[i].swaps = 0.0;                     //--- Initializing swaps
      symbol_data[i].comm = 0.0;                      //--- Initializing commission
      symbol_data[i].buys_str = "0";                  //--- Initializing buys string
      symbol_data[i].sells_str = "0";                 //--- Initializing sells string
      symbol_data[i].trades_str = "0";                //--- Initializing trades string
      symbol_data[i].lots_str = "0.00";               //--- Initializing lots string
      symbol_data[i].profit_str = "0.00";             //--- Initializing profit string
      symbol_data[i].pending_str = "0";               //--- Initializing pending string
      symbol_data[i].swaps_str = "0.00";              //--- Initializing swaps string
      symbol_data[i].comm_str = "0.00";               //--- Initializing commission string
   }
}

カウント関数のロジックを強化するために、ループの方向を増分から減分に変更しました。MQL5では通常、減分ループの方が安全ですが、元の増分ループを維持することも可能です。さらに、注文やポジションを選択する前にチケットのチェックを追加し、無効なチケットやリストの変更による潜在的なエラーを防止しています。完全に動的なダッシュボードを実現するため、初期のダッシュボード作成処理を関数化し、作成方法を2つに分けます。1つは最大化された状態の作成用、もう1つは最小化された状態の作成用です。

//+------------------------------------------------------------------+
//| Creating full dashboard UI                                       |
//+------------------------------------------------------------------+
void createFullDashboard() {
   CollectActiveSymbols();                            //--- Collecting active symbols
   int num_rows = ArraySize(symbol_data);             //--- Getting number of rows
   int num_columns = ArraySize(headers);              //--- Getting number of columns
   int column_width_sum = 0;                          //--- Initializing column width sum
   for(int i = 0; i < num_columns; i++)               //--- Iterating through columns
      column_width_sum += column_widths[i];           //--- Adding column width
   int panel_width = MathMax(settings.header_x_distances[num_columns - 1] + column_widths[num_columns - 1], column_width_sum) + 20 + settings.label_x_offset; //--- Calculating panel width
   // Creating main panel
   string panel_name = PREFIX + PANEL;                //--- Defining main panel name
   createRectangle(panel_name, settings.panel_x, settings.panel_y, panel_width, (num_rows + 3) * settings.row_height, settings.bg_color, settings.border_color); //--- Creating main panel
   // Creating header panel
   string header_panel = PREFIX + HEADER_PANEL;       //--- Defining header panel name
   createRectangle(header_panel, settings.panel_x, settings.panel_y, panel_width, settings.row_height, settings.section_bg_color, settings.border_color); //--- Creating header panel
   // Creating header title
   createLABEL(PREFIX + HEADER_PANEL_TEXT, "Trading Dashboard", settings.panel_x + 10, settings.panel_y + 8 + settings.label_y_offset, clrBlack, 14, settings.font, ANCHOR_LEFT, "Dashboard Title"); //--- Creating header title
   // Creating export button
   createLABEL(PREFIX + EXPORT_BUTTON, CharToString(60), settings.panel_x + panel_width - 90, settings.panel_y + 12, clrBlack, 18, "Wingdings", ANCHOR_CENTER, "Click to export or press 'E' key to export", true); //--- Creating export button
   // Creating toggle button
   createLABEL(PREFIX + TOGGLE_BUTTON, CharToString('r'), settings.panel_x + panel_width - 60, settings.panel_y + 12, clrBlack, 18, "Wingdings", ANCHOR_CENTER, "Minimize dashboard", true); //--- Creating toggle button
   // Creating close button
   createLABEL(PREFIX + CLOSE_BUTTON, CharToString('r'), settings.panel_x + panel_width - 30, settings.panel_y + 12, clrBlack, 18, "Webdings", ANCHOR_CENTER, "Close dashboard", true); //--- Creating close button
   // Creating headers
   int header_y = settings.panel_y + settings.row_height + 8 + settings.label_y_offset; //--- Calculating header y-coordinate
   for(int i = 0; i < num_columns; i++) {             //--- Iterating through headers
      string header_name = PREFIX + HEADER + IntegerToString(i); //--- Defining header label name
      int header_x = settings.panel_x + settings.header_x_distances[i] + settings.label_x_offset; //--- Calculating header x-coordinate
      createLABEL(header_name, headers[i], header_x, header_y, settings.header_color, 12, settings.font, ANCHOR_LEFT, "Click to sort", true); //--- Creating header label
   }
   // Creating symbol and data labels
   int first_row_y = header_y + settings.row_height;  //--- Calculating first row y-coordinate
   int symbol_x = settings.panel_x + 10 + settings.label_x_offset; //--- Setting symbol x-coordinate
   for(int i = 0; i < num_rows; i++) {                //--- Iterating through rows
      string symbol_name = PREFIX + SYMB + IntegerToString(i); //--- Defining symbol label name
      createLABEL(symbol_name, symbol_data[i].name, symbol_x, first_row_y + i * settings.row_height + settings.label_y_offset, settings.text_color, settings.font_size, settings.font, ANCHOR_LEFT, "Symbol name"); //--- Creating symbol label
      int x_offset = settings.panel_x + 10 + column_widths[0] + settings.label_x_offset; //--- Setting data x-offset
      for(int j = 0; j < num_columns - 1; j++) {      //--- Iterating through data columns
         string data_name = PREFIX + DATA + IntegerToString(i) + "_" + IntegerToString(j); //--- Defining data label name
         color initial_color = data_default_colors[j]; //--- Setting initial color
         string initial_txt = (j <= 2 || j == 5) ? "0" : "0.00"; //--- Setting initial text
         createLABEL(data_name, initial_txt, x_offset, first_row_y + i * settings.row_height + settings.label_y_offset, initial_color, settings.font_size, settings.font, ANCHOR_RIGHT, "Data value"); //--- Creating data label
         x_offset += column_widths[j + 1];            //--- Updating x-offset
      }
   }
   // Creating footer panel
   int footer_y = settings.panel_y + (num_rows + 3) * settings.row_height - settings.row_height; //--- Calculating footer y-coordinate
   string footer_panel = PREFIX + FOOTER_PANEL;       //--- Defining footer panel name
   createRectangle(footer_panel, settings.panel_x, footer_y, panel_width, settings.row_height, settings.section_bg_color, settings.border_color); //--- Creating footer panel
   // Creating footer text and data
   int footer_text_x = settings.panel_x + 10 + settings.label_x_offset; //--- Setting footer text x-coordinate
   createLABEL(PREFIX + FOOTER_TEXT, "Total:", footer_text_x, footer_y + 8 + settings.label_y_offset, settings.text_color, settings.font_size, settings.font, ANCHOR_LEFT, "Totals"); //--- Creating footer text label
   int x_offset = settings.panel_x + 10 + column_widths[0] + settings.label_x_offset; //--- Setting footer data x-offset
   for(int j = 0; j < num_columns - 1; j++) {         //--- Iterating through footer data
      string footer_data_name = PREFIX + FOOTER_DATA + IntegerToString(j); //--- Defining footer data label name
      color footer_color = data_default_colors[j];    //--- Setting footer data color
      string initial_txt = (j <= 2 || j == 5) ? "0" : "0.00"; //--- Setting initial text
      createLABEL(footer_data_name, initial_txt, x_offset, footer_y + 8 + settings.label_y_offset, footer_color, settings.font_size, settings.font, ANCHOR_RIGHT, "Total value"); //--- Creating footer data label
      x_offset += column_widths[j + 1];               //--- Updating x-offset
   }
   // Creating account panel
   int account_panel_y = footer_y + settings.row_height + 5; //--- Calculating account panel y-coordinate
   string account_panel_name = PREFIX + ACCOUNT_PANEL; //--- Defining account panel name
   createRectangle(account_panel_name, settings.panel_x, account_panel_y, panel_width, settings.row_height, settings.section_bg_color, settings.border_color); //--- Creating account panel
   // Creating account text and data labels
   int acc_x = settings.panel_x + 10 + settings.label_x_offset; //--- Setting account label x-coordinate
   int acc_data_offset = 160;                         //--- Setting data offset
   int acc_spacing = (panel_width - 45) / ArraySize(account_items); //--- Calculating spacing
   for(int k = 0; k < ArraySize(account_items); k++) { //--- Iterating through account items
      string acc_text_name = PREFIX + ACC_TEXT + IntegerToString(k); //--- Defining account text label name
      int text_x = acc_x + k * acc_spacing;          //--- Calculating text x-coordinate
      createLABEL(acc_text_name, account_items[k] + ":", text_x, account_panel_y + 8 + settings.label_y_offset, settings.text_color, settings.font_size, settings.font, ANCHOR_LEFT, "Account info"); //--- Creating account text label
      string acc_data_name = PREFIX + ACC_DATA + IntegerToString(k); //--- Defining account data label name
      int data_x = text_x + acc_data_offset;         //--- Calculating data x-coordinate
      createLABEL(acc_data_name, "0.00", data_x, account_panel_y + 8 + settings.label_y_offset, settings.text_color, settings.font_size, settings.font, ANCHOR_RIGHT, "Account value"); //--- Creating account data label
   }
   ChartRedraw(0);                                    //--- Redrawing chart
}

//+------------------------------------------------------------------+
//| Creating minimized dashboard UI                                  |
//+------------------------------------------------------------------+
void createMinimizedDashboard() {
   int num_columns = ArraySize(headers);              //--- Getting number of columns
   int column_width_sum = 0;                          //--- Initializing column width sum
   for(int i = 0; i < num_columns; i++)               //--- Iterating through columns
      column_width_sum += column_widths[i];           //--- Adding column width
   int panel_width = MathMax(settings.header_x_distances[num_columns - 1] + column_widths[num_columns - 1], column_width_sum) + 20 + settings.label_x_offset; //--- Calculating panel width
   // Creating header panel
   createRectangle(PREFIX + HEADER_PANEL, settings.panel_x, settings.panel_y, panel_width, settings.row_height, settings.section_bg_color, settings.border_color); //--- Creating header panel
   // Creating header title
   createLABEL(PREFIX + HEADER_PANEL_TEXT, "Trading Dashboard", settings.panel_x + 10, settings.panel_y + 8 + settings.label_y_offset, clrBlack, 14, settings.font, ANCHOR_LEFT, "Dashboard Title"); //--- Creating header title
   // Creating export button
   createLABEL(PREFIX + EXPORT_BUTTON, CharToString(60), settings.panel_x + panel_width - 90, settings.panel_y + 12, clrBlack, 18, "Wingdings", ANCHOR_CENTER, "Click to export or press 'E' key to export", true); //--- Creating export button
   // Creating toggle button (maximize)
   createLABEL(PREFIX + TOGGLE_BUTTON, CharToString('o'), settings.panel_x + panel_width - 60, settings.panel_y + 12, clrBlack, 18, "Wingdings", ANCHOR_CENTER, "Maximize dashboard", true); //--- Creating toggle button
   // Creating close button
   createLABEL(PREFIX + CLOSE_BUTTON, CharToString('r'), settings.panel_x + panel_width - 30, settings.panel_y + 12, clrBlack, 18, "Webdings", ANCHOR_CENTER, "Close dashboard", true); //--- Creating close button
   ChartRedraw(0);                                    //--- Redrawing chart
}

//+------------------------------------------------------------------+
//| Deleting all dashboard objects                                   |
//+------------------------------------------------------------------+
void deleteAllObjects() {
   ObjectsDeleteAll(0, PREFIX, -1, -1);               //--- Deleting all objects with prefix
}

UIを管理し、フル表示と最小化表示の両方に対応したインタラクティブ要素を提供するために、createFullDashboard、createMinimizedDashboard、deleteAllObjects関数を実装します。createFullDashboardでは、まずCollectActiveSymbolsを呼び出してsymbol_dataを取得し、ArraySizeでnum_rowsとnum_columnsを計算します。また、column_widthsとsettings.header_x_distancesを使用してpanel_widthを算出します。メインパネルはcreateRectangleを使用して「PREFIX + PANEL」で作成し、ヘッダーパネルは「PREFIX + HEADER_PANEL」、ヘッダーのタイトルはcreateLABELで「PREFIX + HEADER_PANEL_TEXT」として「Trading Dashboard」として作成します。

さらに、ボタンはcreateLABELで追加します。「PREFIX + EXPORT_BUTTON」(Wingdings 60)、「PREFIX + TOGGLE_BUTTON」(最小化用にWingdings「r」)、「PREFIX + CLOSE_BUTTON」(Webdings「r」)を作成し、それぞれに特定のツールチップを設定し、選択可能にします。アイコンスタイルの選択は任意ですが、フォントに使用できるものを簡潔に示します。フォントやシンボルは正確なものを使用してください。

シンボルフォント

次に、ヘッダーラベルはheadersを計算済みの位置に作成し、銘柄ラベルはsymbol_data[i].name用に作成します。データラベルは初期値を設定し、フッターパネルは「PREFIX + FOOTER_PANEL」で作成、フッターテキストは「Total:」、フッターデータラベル、口座パネルは「PREFIX + ACCOUNT_PANEL」、口座ラベルはaccount_items用に作成します。すべてcreateRectangleとcreateLABELを使用し、適切な座標と色を指定した後、ChartRedrawを実行します。

createMinimizedDashboardでは、ヘッダーパネルのみのコンパクトなUIを作成します。createRectangleで「PREFIX + HEADER_PANEL」を作成し、createLABELで「PREFIX + HEADER_PANEL_TEXT」としてヘッダータイトルを作成します。また、ボタンはエクスポート(Wingdings 60)、トグル(最大化用にWingdings「o」)、クローズ(Webdings「r」)を作成し、画面の使用を最小限に抑えてChartRedrawを実行します。

deleteAllObjects関数では、ObjectsDeleteAllを使用してPREFIXに関連するすべてのチャートとタイプのダッシュボードオブジェクトを削除し、UI更新や閉鎖時にクリーンな状態を保証します。これらの関数により、フル表示と最小化表示に対応した柔軟なダッシュボードを実現でき、ドラッグやトグルなどのユーザー操作をサポートします。次に、これらの動的関数を用いてダッシュボード関数を更新していきます。

//+------------------------------------------------------------------+
//| Updating dashboard data and visuals                              |
//+------------------------------------------------------------------+
void UpdateDashboard() {
   bool needs_redraw = false;                //--- Initializing redraw flag
   CollectActiveSymbols();                   //--- Collecting active symbols
   int current_num = ArraySize(symbol_data); //--- Getting current number of symbols
   if(current_num != prev_num_symbols) {     //--- Checking if symbol count changed
      deleteAllObjects();                    //--- Deleting all objects
      if(panel_minimized) {                  //--- Checking if minimized
         createMinimizedDashboard();         //--- Creating minimized dashboard
      } else {
         createFullDashboard();              //--- Creating full dashboard
      }
      prev_num_symbols = current_num;        //--- Updating previous symbol count
      needs_redraw = true;                   //--- Setting redraw flag
   }
   if(!panel_is_visible || panel_minimized) return; //--- Exiting if not visible or minimized
   // Resetting totals
   totalBuys = 0;     //--- Resetting total buys
   totalSells = 0;    //--- Resetting total sells
   totalTrades = 0;   //--- Resetting total trades
   totalLots = 0.0;   //--- Resetting total lots
   totalProfit = 0.0; //--- Resetting total profit
   totalPending = 0;  //--- Resetting total pending
   totalSwap = 0.0;   //--- Resetting total swap
   totalComm = 0.0;   //--- Resetting total commission
   // Calculating symbol data and totals
   for(int i = 0; i < current_num; i++) {           //--- Iterating through symbols
      string symbol = symbol_data[i].name;          //--- Getting symbol name
      for(int j = 0; j < 8; j++) {                  //--- Iterating through data columns
         string value = "";                         //--- Initializing value
         color data_color = data_default_colors[j]; //--- Setting default color
         double dval = 0.0;                         //--- Initializing double value
         int ival = 0;                              //--- Initializing integer value
         switch(j) {                                //--- Handling data type
            case 0:                                 // Buy positions
               value = countPositions(symbol, POSITION_TYPE_BUY); //--- Getting buy positions
               ival = (int)StringToInteger(value);  //--- Converting to integer
               if(value != symbol_data[i].buys_str) { //--- Checking if changed
                  symbol_data[i].buys_str = value;  //--- Updating buys string
                  symbol_data[i].buys = ival;       //--- Updating buys count
               }
               totalBuys += ival;                   //--- Adding to total buys
               break;
            case 1:                                 // Sell positions
               value = countPositions(symbol, POSITION_TYPE_SELL); //--- Getting sell positions
               ival = (int)StringToInteger(value);  //--- Converting to integer
               if(value != symbol_data[i].sells_str) { //--- Checking if changed
                  symbol_data[i].sells_str = value; //--- Updating sells string
                  symbol_data[i].sells = ival;      //--- Updating sells count
               }
               totalSells += ival; //--- Adding to total sells
               break;
            case 2: // Total trades
               value = countPositionsTotal(symbol); //--- Getting total trades
               ival = (int)StringToInteger(value);  //--- Converting to integer
               if(value != symbol_data[i].trades_str) { //--- Checking if changed
                  symbol_data[i].trades_str = value; //--- Updating trades string
                  symbol_data[i].trades = ival;     //--- Updating trades count
               }
               totalTrades += ival;                 //--- Adding to total trades
               break;
            case 3: // Lots
               value = sumPositionDouble(symbol, POSITION_VOLUME); //--- Getting total lots
               dval = StringToDouble(value);        //--- Converting to double
               if(value != symbol_data[i].lots_str) { //--- Checking if changed
                  symbol_data[i].lots_str = value; //--- Updating lots string
                  symbol_data[i].lots = dval;      //--- Updating lots value
               }
               totalLots += dval;                  //--- Adding to total lots
               break;
            case 4: // Profit
               value = sumPositionDouble(symbol, POSITION_PROFIT); //--- Getting total profit
               dval = StringToDouble(value);      //--- Converting to double
               data_color = (dval > 0) ? clrGreen : (dval < 0) ? clrRed : clrGray; //--- Setting color based on value
               if(value != symbol_data[i].profit_str) { //--- Checking if changed
                  symbol_data[i].profit_str = value; //--- Updating profit string
                  symbol_data[i].profit = dval;  //--- Updating profit value
               }
               totalProfit += dval;              //--- Adding to total profit
               break;
            case 5: // Pending
               value = countOrders(symbol);      //--- Getting pending orders
               ival = (int)StringToInteger(value); //--- Converting to integer
               if(value != symbol_data[i].pending_str) { //--- Checking if changed
                  symbol_data[i].pending_str = value; //--- Updating pending string
                  symbol_data[i].pending = ival; //--- Updating pending count
               }
               totalPending += ival;             //--- Adding to total pending
               break;
            case 6: // Swap
               value = sumPositionDouble(symbol, POSITION_SWAP); //--- Getting total swap
               dval = StringToDouble(value);     //--- Converting to double
               data_color = (dval > 0) ? clrGreen : (dval < 0) ? clrRed : clrPurple; //--- Setting color based on value
               if(value != symbol_data[i].swaps_str) { //--- Checking if changed
                  symbol_data[i].swaps_str = value;    //--- Updating swap string
                  symbol_data[i].swaps = dval;  //--- Updating swap value
               }
               totalSwap += dval; //--- Adding to total swap
               break;
            case 7: // Comm
               dval = sumPositionCommission(symbol); //--- Getting total commission
               value = DoubleToString(dval, 2);      //--- Formatting commission
               data_color = (dval > 0) ? clrGreen : (dval < 0) ? clrRed : clrBrown; //--- Setting color based on value
               if(value != symbol_data[i].comm_str) { //--- Checking if changed
                  symbol_data[i].comm_str = value;   //--- Updating commission string
                  symbol_data[i].comm = dval;        //--- Updating commission value
               }
               totalComm += dval;                    //--- Adding to total commission
               break;
         }
      }
   }
   // Sort after calculating values
   SortDashboard(); //--- Sorting dashboard data
   // Update header breathing effect
   glow_counter += MathMax(UpdateIntervalMs, 10); //--- Incrementing glow counter
   if(glow_counter >= GLOW_INTERVAL_MS) { //--- Checking if glow interval reached
      if(glow_direction) { //--- Checking if glowing forward
         glow_index++; //--- Incrementing glow index
         if(glow_index >= ArraySize(settings.header_shades) - 1) //--- Checking if at end
            glow_direction = false; //--- Reversing glow direction
      } else { //--- Glow backward
         glow_index--; //--- Decrementing glow index
         if(glow_index <= 0) //--- Checking if at start
            glow_direction = true; //--- Reversing glow direction
      }
      glow_counter = 0; //--- Resetting glow counter
   }
   color header_shade = settings.header_shades[glow_index]; //--- Getting current header shade
   for(int i = 0; i < ArraySize(headers); i++) { //--- Iterating through headers
      string header_name = PREFIX + HEADER + IntegerToString(i); //--- Defining header name
      ObjectSetInteger(0, header_name, OBJPROP_COLOR, header_shade); //--- Updating header color
      needs_redraw = true; //--- Setting redraw flag
   }
   // Update symbol and data labels
   bool labels_updated = false; //--- Initializing label update flag
   for(int i = 0; i < current_num; i++) { //--- Iterating through symbols
      string symbol = symbol_data[i].name; //--- Getting symbol name
      string symb_name = PREFIX + SYMB + IntegerToString(i); //--- Defining symbol label name
      string current_symb_txt = ObjectGetString(0, symb_name, OBJPROP_TEXT); //--- Getting current symbol text
      if(current_symb_txt != symbol) { //--- Checking if symbol changed
         ObjectSetString(0, symb_name, OBJPROP_TEXT, symbol); //--- Updating symbol text
         labels_updated = true; //--- Setting label updated flag
      }
      for(int j = 0; j < 8; j++) { //--- Iterating through data columns
         string data_name = PREFIX + DATA + IntegerToString(i) + "_" + IntegerToString(j); //--- Defining data label name
         string value; //--- Initializing value
         color data_color = data_default_colors[j]; //--- Setting default color
         switch(j) { //--- Handling data type
            case 0: //--- Buy positions
               value = symbol_data[i].buys_str; //--- Getting buys string
               data_color = clrRed; //--- Setting color to red
               break;
            case 1: //--- Sell positions
               value = symbol_data[i].sells_str; //--- Getting sells string
               data_color = clrGreen; //--- Setting color to green
               break;
            case 2: // Total trades
               value = symbol_data[i].trades_str;
               data_color = clrDarkGray;
               break;               
            case 3: //--- Lots
               value = symbol_data[i].lots_str; //--- Getting lots string
               data_color = clrOrange; //--- Setting color to orange
               break;
            case 4: //--- Profit
               value = symbol_data[i].profit_str; //--- Getting profit string
               data_color = (symbol_data[i].profit > 0) ? clrGreen : (symbol_data[i].profit < 0) ? clrRed : clrGray; //--- Setting color based on profit
               break;
            case 5: //--- Pending
               value = symbol_data[i].pending_str; //--- Getting pending string
               data_color = clrBlue; //--- Setting color to blue
               break;
            case 6: //--- Swap
               value = symbol_data[i].swaps_str; //--- Getting swap string
               data_color = (symbol_data[i].swaps > 0) ? clrGreen : (symbol_data[i].swaps < 0) ? clrRed : clrPurple; //--- Setting color based on swap
               break;
            case 7: //--- Comm
               value = symbol_data[i].comm_str; //--- Getting commission string
               data_color = (symbol_data[i].comm > 0) ? clrGreen : (symbol_data[i].comm < 0) ? clrRed : clrBrown; //--- Setting color based on commission
               break;
         }
         if(updateLABEL(data_name, value, data_color)) labels_updated = true; //--- Updating label if changed
      }
   }
   if(labels_updated) needs_redraw = true; //--- Setting redraw flag if labels updated
   // Updating totals
   string new_total_buys = IntegerToString(totalBuys); //--- Formatting total buys
   if(new_total_buys != total_buys_str) { //--- Checking if changed
      total_buys_str = new_total_buys; //--- Updating buys string
      if(updateLABEL(PREFIX + FOOTER_DATA + "0", new_total_buys, clrRed)) needs_redraw = true; //--- Updating label
   }
   string new_total_sells = IntegerToString(totalSells); //--- Formatting total sells
   if(new_total_sells != total_sells_str) { //--- Checking if changed
      total_sells_str = new_total_sells; //--- Updating sells string
      if(updateLABEL(PREFIX + FOOTER_DATA + "1", new_total_sells, clrGreen)) needs_redraw = true; //--- Updating label
   }
   string new_total_trades = IntegerToString(totalTrades); //--- Formatting total trades
   if(new_total_trades != total_trades_str) { //--- Checking if changed
      total_trades_str = new_total_trades; //--- Updating trades string
      if(updateLABEL(PREFIX + FOOTER_DATA + "2", new_total_trades, clrDarkGray)) needs_redraw = true; //--- Updating label
   }
   string new_total_lots = DoubleToString(totalLots, 2); //--- Formatting total lots
   if(new_total_lots != total_lots_str) { //--- Checking if changed
      total_lots_str = new_total_lots; //--- Updating lots string
      if(updateLABEL(PREFIX + FOOTER_DATA + "3", new_total_lots, clrOrange)) needs_redraw = true; //--- Updating label
   }
   string new_total_profit = DoubleToString(totalProfit, 2); //--- Formatting total profit
   color total_profit_color = (totalProfit > 0) ? clrGreen : (totalProfit < 0) ? clrRed : clrGray; //--- Setting color based on profit
   if(new_total_profit != total_profit_str) { //--- Checking if changed
      total_profit_str = new_total_profit; //--- Updating profit string
      if(updateLABEL(PREFIX + FOOTER_DATA + "4", new_total_profit, total_profit_color)) needs_redraw = true; //--- Updating label
   }
   string new_total_pending = IntegerToString(totalPending); //--- Formatting total pending
   if(new_total_pending != total_pending_str) { //--- Checking if changed
      total_pending_str = new_total_pending; //--- Updating pending string
      if(updateLABEL(PREFIX + FOOTER_DATA + "5", new_total_pending, clrBlue)) needs_redraw = true; //--- Updating label
   }
   string new_total_swap = DoubleToString(totalSwap, 2); //--- Formatting total swap
   color total_swap_color = (totalSwap > 0) ? clrGreen : (totalSwap < 0) ? clrRed : clrPurple; //--- Setting color based on swap
   if(new_total_swap != total_swap_str) { //--- Checking if changed
      total_swap_str = new_total_swap; //--- Updating swap string
      if(updateLABEL(PREFIX + FOOTER_DATA + "6", new_total_swap, total_swap_color)) needs_redraw = true; //--- Updating label
   }
   string new_total_comm = DoubleToString(totalComm, 2); //--- Formatting total commission
   color total_comm_color = (totalComm > 0) ? clrGreen : (totalComm < 0) ? clrRed : clrBrown; //--- Setting color based on commission
   if(new_total_comm != total_comm_str) { //--- Checking if changed
      total_comm_str = new_total_comm; //--- Updating commission string
      if(updateLABEL(PREFIX + FOOTER_DATA + "7", new_total_comm, total_comm_color)) needs_redraw = true; //--- Updating label
   }
   // Updating account info
   double balance = AccountInfoDouble(ACCOUNT_BALANCE); //--- Getting account balance
   double equity = AccountInfoDouble(ACCOUNT_EQUITY); //--- Getting account equity
   double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); //--- Getting free margin
   string new_bal = DoubleToString(balance, 2); //--- Formatting balance
   if(new_bal != acc_bal_str) { //--- Checking if changed
      acc_bal_str = new_bal; //--- Updating balance string
      if(updateLABEL(PREFIX + ACC_DATA + "0", new_bal, clrBlack)) needs_redraw = true; //--- Updating label
   }
   string new_eq = DoubleToString(equity, 2); //--- Formatting equity
   color eq_color = (equity > balance) ? clrGreen : (equity < balance) ? clrRed : clrBlack; //--- Setting color based on equity
   if(new_eq != acc_eq_str) { //--- Checking if changed
      acc_eq_str = new_eq; //--- Updating equity string
      if(updateLABEL(PREFIX + ACC_DATA + "1", new_eq, eq_color)) needs_redraw = true; //--- Updating label
   }
   string new_free = DoubleToString(free_margin, 2); //--- Formatting free margin
   if(new_free != acc_free_str) { //--- Checking if changed
      acc_free_str = new_free; //--- Updating free margin string
      if(updateLABEL(PREFIX + ACC_DATA + "2", new_free, clrBlack)) needs_redraw = true; //--- Updating label
   }
   if(needs_redraw) { //--- Checking if redraw needed
      ChartRedraw(0); //--- Redrawing chart
   }
}

UpdateDashboard関数には、処理の先頭でCollectActiveSymbolsを呼び出す処理を追加しました。これにより、常に合計値や残高フィールドが更新されるようになります。次に、銘柄数が変化した場合はdeleteAllObjects関数を呼び出してダッシュボードを破棄し、createFullDashboardまたはcreateMinimizedDashboard関数を通じて再作成します。データ計算ループ内では、利益、スワップ、手数料の色付けロジックを追加しました。以前は部分的にしか反映されていませんでした。変更箇所は識別しやすいようにハイライトしています。最後に、初期化時にこのロジックを呼び出すことで、今回のマイルストーンとなるダッシュボードの完成を確認できます。

//+------------------------------------------------------------------+
//| Initializing expert                                              |
//+------------------------------------------------------------------+
int OnInit() {
   createFullDashboard();                             //--- Creating full dashboard
   prev_num_symbols = ArraySize(symbol_data);         //--- Setting initial symbol count
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);  //--- Enabling mouse move events
   EventSetMillisecondTimer(MathMax(UpdateIntervalMs, 10)); //--- Setting timer with minimum 10ms
   UpdateDashboard();                                 //--- Updating dashboard
   return(INIT_SUCCEEDED);                            //--- Returning success
}

//+------------------------------------------------------------------+
//| Deinitializing expert                                            |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   deleteAllObjects();                                //--- Deleting all objects
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); //--- Disabling mouse move events
   EventKillTimer();                                  //--- Stopping timer
}

//+------------------------------------------------------------------+
//| Handling timer for millisecond-based updates                     |
//+------------------------------------------------------------------+
void OnTimer() {
   if(panel_is_visible && !panel_minimized) {         //--- Checking if visible and not minimized
      UpdateDashboard();                              //--- Updating dashboard
   }
}

ここでは、OnInit、OnDeinit、OnTimerイベントハンドラを実装し、ダッシュボードのライフサイクルと更新を管理することで、インタラクティブかつ動的な機能を実現します。OnInit関数では、まずcreateFullDashboardを呼び出して完全なUIを構築し、prev_num_symbolsをArraySizeを用いてsymbol_dataのサイズに設定し、初期銘柄を追跡します。さらに、ChartSetIntegerCHART_EVENT_MOUSE_MOVEをtrueに設定してマウス移動イベントを有効化し、ドラッグやホバーエフェクトを可能にします。EventSetMillisecondTimerでUpdateIntervalMsと10msの最大値をタイマーに設定し、定期的な更新をおこないます。そして、UpdateDashboardを呼び出して初期データを反映させ、成功時にはINIT_SUCCEEDEDを返します。

OnDeinit関数では、deleteAllObjectsを呼び出してPREFIXに関連するすべてのダッシュボードオブジェクトを削除し、ChartSetIntegerでCHART_EVENT_MOUSE_MOVEをfalseに設定してマウス移動イベントを無効化し、EventKillTimerでタイマーを停止してリソースを解放します。

OnTimer関数では、panel_is_visibleがtrueでpanel_minimizedがfalseの場合にのみUpdateDashboardを呼び出し、ダッシュボードが完全に表示されているときだけデータを更新します。これにより、最小化または非表示状態では処理をおこなわず、効率的な更新を実現します。コンパイル後、初期化時には以下のような成果が確認できます。

初期化関数

画像からも、新機能が正常に表示されていることが確認できます。次に、ドラッグ時にオブジェクトを再作成せずにパネル位置を更新できる関数の作成に進みます。

//+------------------------------------------------------------------+
//| Updating panel object positions                                  |
//+------------------------------------------------------------------+
void updatePanelPositions() {
   int num_rows = ArraySize(symbol_data);             //--- Getting number of rows
   int num_columns = ArraySize(headers);              //--- Getting number of columns
   int column_width_sum = 0;                          //--- Initializing column width sum
   for(int i = 0; i < num_columns; i++)               //--- Iterating through columns
      column_width_sum += column_widths[i];           //--- Adding column width
   int panel_width = MathMax(settings.header_x_distances[num_columns - 1] + column_widths[num_columns - 1], column_width_sum) + 20 + settings.label_x_offset; //--- Calculating panel width
   // Updating header panel and buttons
   ObjectSetInteger(0, PREFIX + HEADER_PANEL, OBJPROP_XDISTANCE, settings.panel_x); //--- Updating header panel x-coordinate
   ObjectSetInteger(0, PREFIX + HEADER_PANEL, OBJPROP_YDISTANCE, settings.panel_y); //--- Updating header panel y-coordinate
   ObjectSetInteger(0, PREFIX + HEADER_PANEL_TEXT, OBJPROP_XDISTANCE, settings.panel_x + 10); //--- Updating header text x-coordinate
   ObjectSetInteger(0, PREFIX + HEADER_PANEL_TEXT, OBJPROP_YDISTANCE, settings.panel_y + 8 + settings.label_y_offset); //--- Updating header text y-coordinate
   ObjectSetInteger(0, PREFIX + EXPORT_BUTTON, OBJPROP_XDISTANCE, settings.panel_x + panel_width - 90); //--- Updating export button x-coordinate
   ObjectSetInteger(0, PREFIX + EXPORT_BUTTON, OBJPROP_YDISTANCE, settings.panel_y + 12); //--- Updating export button y-coordinate
   ObjectSetInteger(0, PREFIX + TOGGLE_BUTTON, OBJPROP_XDISTANCE, settings.panel_x + panel_width - 60); //--- Updating toggle button x-coordinate
   ObjectSetInteger(0, PREFIX + TOGGLE_BUTTON, OBJPROP_YDISTANCE, settings.panel_y + 12); //--- Updating toggle button y-coordinate
   ObjectSetInteger(0, PREFIX + CLOSE_BUTTON, OBJPROP_XDISTANCE, settings.panel_x + panel_width - 30); //--- Updating close button x-coordinate
   ObjectSetInteger(0, PREFIX + CLOSE_BUTTON, OBJPROP_YDISTANCE, settings.panel_y + 12); //--- Updating close button y-coordinate
   if(!panel_minimized) {                                               //--- Checking if not minimized
      // Updating main panel
      ObjectSetInteger(0, PREFIX + PANEL, OBJPROP_XDISTANCE, settings.panel_x); //--- Updating main panel x-coordinate
      ObjectSetInteger(0, PREFIX + PANEL, OBJPROP_YDISTANCE, settings.panel_y); //--- Updating main panel y-coordinate
      // Updating headers
      int header_y = settings.panel_y + settings.row_height + 8 + settings.label_y_offset; //--- Calculating header y-coordinate
      for(int i = 0; i < num_columns; i++) {                            //--- Iterating through headers
         string header_name = PREFIX + HEADER + IntegerToString(i);     //--- Defining header name
         int header_x = settings.panel_x + settings.header_x_distances[i] + settings.label_x_offset; //--- Calculating header x-coordinate
         ObjectSetInteger(0, header_name, OBJPROP_XDISTANCE, header_x); //--- Updating header x-coordinate
         ObjectSetInteger(0, header_name, OBJPROP_YDISTANCE, header_y); //--- Updating header y-coordinate
      }
      // Updating symbol and data labels
      int first_row_y = header_y + settings.row_height;                 //--- Calculating first row y-coordinate
      int symbol_x = settings.panel_x + 10 + settings.label_x_offset;   //--- Setting symbol x-coordinate
      for(int i = 0; i < num_rows; i++) {                               //--- Iterating through rows
         string symbol_name = PREFIX + SYMB + IntegerToString(i);       //--- Defining symbol label name
         ObjectSetInteger(0, symbol_name, OBJPROP_XDISTANCE, symbol_x); //--- Updating symbol x-coordinate
         ObjectSetInteger(0, symbol_name, OBJPROP_YDISTANCE, first_row_y + i * settings.row_height + settings.label_y_offset); //--- Updating symbol y-coordinate
         int x_offset = settings.panel_x + 10 + column_widths[0] + settings.label_x_offset; //--- Setting data x-offset
         for(int j = 0; j < num_columns - 1; j++) {                      //--- Iterating through data columns
            string data_name = PREFIX + DATA + IntegerToString(i) + "_" + IntegerToString(j); //--- Defining data label name
            ObjectSetInteger(0, data_name, OBJPROP_XDISTANCE, x_offset); //--- Updating data x-coordinate
            ObjectSetInteger(0, data_name, OBJPROP_YDISTANCE, first_row_y + i * settings.row_height + settings.label_y_offset); //--- Updating data y-coordinate
            x_offset += column_widths[j + 1];                            //--- Updating x-offset
         }
      }
      // Updating footer panel and labels
      int footer_y = settings.panel_y + (num_rows + 3) * settings.row_height - settings.row_height; //--- Calculating footer y-coordinate
      ObjectSetInteger(0, PREFIX + FOOTER_PANEL, OBJPROP_XDISTANCE, settings.panel_x); //--- Updating footer panel x-coordinate
      ObjectSetInteger(0, PREFIX + FOOTER_PANEL, OBJPROP_YDISTANCE, footer_y); //--- Updating footer panel y-coordinate
      ObjectSetInteger(0, PREFIX + FOOTER_TEXT, OBJPROP_XDISTANCE, settings.panel_x + 10 + settings.label_x_offset); //--- Updating footer text x-coordinate
      ObjectSetInteger(0, PREFIX + FOOTER_TEXT, OBJPROP_YDISTANCE, footer_y + 8 + settings.label_y_offset); //--- Updating footer text y-coordinate
      int x_offset = settings.panel_x + 10 + column_widths[0] + settings.label_x_offset; //--- Setting footer data x-offset
      for(int j = 0; j < num_columns - 1; j++) {                        //--- Iterating through footer data
         string footer_data_name = PREFIX + FOOTER_DATA + IntegerToString(j); //--- Defining footer data name
         ObjectSetInteger(0, footer_data_name, OBJPROP_XDISTANCE, x_offset);  //--- Updating footer data x-coordinate
         ObjectSetInteger(0, footer_data_name, OBJPROP_YDISTANCE, footer_y + 8 + settings.label_y_offset); //--- Updating footer data y-coordinate
         x_offset += column_widths[j + 1];                              //--- Updating x-offset
      }
      // Updating account panel and labels
      int account_panel_y = footer_y + settings.row_height + 5;         //--- Calculating account panel y-coordinate
      ObjectSetInteger(0, PREFIX + ACCOUNT_PANEL, OBJPROP_XDISTANCE, settings.panel_x); //--- Updating account panel x-coordinate
      ObjectSetInteger(0, PREFIX + ACCOUNT_PANEL, OBJPROP_YDISTANCE, account_panel_y);  //--- Updating account panel y-coordinate
      int acc_x = settings.panel_x + 10 + settings.label_x_offset;      //--- Setting account label x-coordinate
      int acc_data_offset = 160;                                        //--- Setting data offset
      int acc_spacing = (panel_width - 45) / ArraySize(account_items);  //--- Calculating spacing
      for(int k = 0; k < ArraySize(account_items); k++) { //--- Iterating through account items
         string acc_text_name = PREFIX + ACC_TEXT + IntegerToString(k); //--- Defining account text name
         int text_x = acc_x + k * acc_spacing;                          //--- Calculating text x-coordinate
         ObjectSetInteger(0, acc_text_name, OBJPROP_XDISTANCE, text_x); //--- Updating account text x-coordinate
         ObjectSetInteger(0, acc_text_name, OBJPROP_YDISTANCE, account_panel_y + 8 + settings.label_y_offset); //--- Updating account text y-coordinate
         string acc_data_name = PREFIX + ACC_DATA + IntegerToString(k); //--- Defining account data name
         int data_x = text_x + acc_data_offset;                         //--- Calculating data x-coordinate
         ObjectSetInteger(0, acc_data_name, OBJPROP_XDISTANCE, data_x); //--- Updating account data x-coordinate
         ObjectSetInteger(0, acc_data_name, OBJPROP_YDISTANCE, account_panel_y + 8 + settings.label_y_offset); //--- Updating account data y-coordinate
      }
   }
   ChartRedraw(0);                                    //--- Redrawing chart
}

ここでは、updatePanelPositions関数を実装し、拡張されたダッシュボードのドラッグ機能を有効にします。これにより、ダッシュボードをドラッグした際にすべてのUI要素が一体となって移動するようになります。まず、symbol_dataとheadersに対してArraySizeを使用してnum_rowsとnum_columnsを計算し、column_widthsを合計し、settings.header_x_distancesとパディングを加えてMathMaxでpanel_widthを算出します。ヘッダーパネルやボタンの位置は、ObjectSetIntegerOBJPROP_XDISTANCEとOBJPROP_YDISTANCEを設定することで更新します。対象は「PREFIX + HEADER_PANEL」、「PREFIX + HEADER_PANEL_TEXT」、「PREFIX + EXPORT_BUTTON」、「PREFIX + TOGGLE_BUTTON」、「PREFIX + CLOSE_BUTTON」で、設定座標にはsettings.panel_xとsettings.panel_yを使用します。

panel_minimizedがfalseの場合、メインパネルの位置は「PREFIX + PANEL」で更新し、ヘッダーはheader_yを「settings.panel_y + settings.row_height + 8 + settings.label_y_offset」から計算して配置します。銘柄およびデータラベルはfirst_row_yにsymbol_xとx_offsetをcolumn_widthsで調整して配置します。フッターパネルとラベルは「num_rows + 3」から計算したfooter_yに配置し、アカウントパネルとラベルはaccount_panel_yにacc_xとacc_spacingを使用して整列させます。すべてObjectSetIntegerを使用して更新し、最後にChartRedrawを呼び出して表示をリフレッシュします。これにより、ドラッグ中でもダッシュボード全体がシームレスに移動し、レイアウトの整合性が維持されます。次に、ヘッダーやボタン上でのカーソル位置を追跡するロジックを定義する必要があります。以下がその実装に使用したロジックです。

//+------------------------------------------------------------------+
//| Checking if cursor is inside header or buttons                   |
//+------------------------------------------------------------------+
bool isCursorInHeaderOrButtons(int mouse_x, int mouse_y) {
   int header_x = (int)ObjectGetInteger(0, PREFIX + HEADER_PANEL, OBJPROP_XDISTANCE);  //--- Getting header x-coordinate
   int header_y = (int)ObjectGetInteger(0, PREFIX + HEADER_PANEL, OBJPROP_YDISTANCE);  //--- Getting header y-coordinate
   int header_width = (int)ObjectGetInteger(0, PREFIX + HEADER_PANEL, OBJPROP_XSIZE);  //--- Getting header width
   int header_height = (int)ObjectGetInteger(0, PREFIX + HEADER_PANEL, OBJPROP_YSIZE); //--- Getting header height
   bool in_header = (mouse_x >= header_x && mouse_x <= header_x + header_width &&
                     mouse_y >= header_y && mouse_y <= header_y + header_height);      //--- Checking if in header
   int close_x = (int)ObjectGetInteger(0, PREFIX + CLOSE_BUTTON, OBJPROP_XDISTANCE);   //--- Getting close button x-coordinate
   int close_y = (int)ObjectGetInteger(0, PREFIX + CLOSE_BUTTON, OBJPROP_YDISTANCE);   //--- Getting close button y-coordinate
   int close_width = 20;                              //--- Setting close button width
   int close_height = 20;                             //--- Setting close button height
   bool in_close = (mouse_x >= close_x && mouse_x <= close_x + close_width &&
                    mouse_y >= close_y && mouse_y <= close_y + close_height);          //--- Checking if in close button
   int export_x = (int)ObjectGetInteger(0, PREFIX + EXPORT_BUTTON, OBJPROP_XDISTANCE); //--- Getting export button x-coordinate
   int export_y = (int)ObjectGetInteger(0, PREFIX + EXPORT_BUTTON, OBJPROP_YDISTANCE); //--- Getting export button y-coordinate
   int export_width = 20;                             //--- Setting export button width
   int export_height = 20;                            //--- Setting export button height
   bool in_export = (mouse_x >= export_x && mouse_x <= export_x + export_width &&
                     mouse_y >= export_y && mouse_y <= export_y + export_height);      //--- Checking if in export button
   int toggle_x = (int)ObjectGetInteger(0, PREFIX + TOGGLE_BUTTON, OBJPROP_XDISTANCE); //--- Getting toggle button x-coordinate
   int toggle_y = (int)ObjectGetInteger(0, PREFIX + TOGGLE_BUTTON, OBJPROP_YDISTANCE); //--- Getting toggle button y-coordinate
   int toggle_width = 20;                             //--- Setting toggle button width
   int toggle_height = 20;                            //--- Setting toggle button height
   bool in_toggle = (mouse_x >= toggle_x && mouse_x <= toggle_x + toggle_width &&
                     mouse_y >= toggle_y && mouse_y <= toggle_y + toggle_height);      //--- Checking if in toggle button
   return in_header || in_close || in_export || in_toggle;                             //--- Returning combined check
}

ここでは、isCursorInHeaderOrButtons関数を実装し、マウスカーソルがインタラクティブ要素上にあるかを検出できるようにします。これにより、ドラッグ操作やボタン操作が可能になります。まず、ObjectGetIntegerを使用して「PREFIX + HEADER_PANEL」の座標とサイズを取得し、OBJPROP_XDISTANCE、OBJPROP_YDISTANCE、OBJPROP_XSIZE、OBJPROP_YSIZEをheader_x、header_y、header_width、header_heightに格納します。その後、カーソル位置(mouse_x, mouse_y)がヘッダーの範囲内にあるかをin_headerで確認します。

同様に、「PREFIX + CLOSE_BUTTON」、「PREFIX + EXPORT_BUTTON」、「PREFIX + TOGGLE_BUTTON」の座標をOBJPROP_XDISTANCEとOBJPROP_YDISTANCEで取得し、close_width、close_height、export_width、export_height、toggle_width、toggle_heightを20に設定します。カーソルが各ボタンの範囲内にあるかをin_close、in_export、in_toggleで確認します。カーソルがヘッダーまたはいずれかのボタン上にある場合は、OR演算子で条件を組み合わせてtrueを返します。ホバー検出後は、視覚的フィードバックのために検出されたヘッダーやボタンの状態を更新する必要があります。これを実現するために、以下のロジックを実装しています。

//+------------------------------------------------------------------+
//| Updating button hover states                                     |
//+------------------------------------------------------------------+
void updateButtonHoverStates(int mouse_x, int mouse_y) {
   int close_x = (int)ObjectGetInteger(0, PREFIX + CLOSE_BUTTON, OBJPROP_XDISTANCE);     //--- Getting close button x-coordinate
   int close_y = (int)ObjectGetInteger(0, PREFIX + CLOSE_BUTTON, OBJPROP_YDISTANCE);     //--- Getting close button y-coordinate
   int close_width = 20;                              //--- Setting close button width
   int close_height = 20;                             //--- Setting close button height
   bool is_close_hovered = (mouse_x >= close_x && mouse_x <= close_x + close_width &&
                            mouse_y >= close_y && mouse_y <= close_y + close_height);     //--- Checking if close button hovered
   if(is_close_hovered != prev_close_hovered) {       //--- Checking hover change
      ObjectSetInteger(0, PREFIX + CLOSE_BUTTON, OBJPROP_COLOR, is_close_hovered ? clrRed : clrBlack); //--- Updating close button color
      ObjectSetInteger(0, PREFIX + CLOSE_BUTTON, OBJPROP_BGCOLOR, is_close_hovered ? clrDodgerBlue : clrNONE); //--- Updating close button background
      prev_close_hovered = is_close_hovered;          //--- Updating previous hover state
      ChartRedraw(0);                                 //--- Redrawing chart
   }
   int export_x = (int)ObjectGetInteger(0, PREFIX + EXPORT_BUTTON, OBJPROP_XDISTANCE);    //--- Getting export button x-coordinate
   int export_y = (int)ObjectGetInteger(0, PREFIX + EXPORT_BUTTON, OBJPROP_YDISTANCE);    //--- Getting export button y-coordinate
   int export_width = 20;                             //--- Setting export button width
   int export_height = 20;                            //--- Setting export button height
   bool is_export_hovered = (mouse_x >= export_x && mouse_x <= export_x + export_width &&
                             mouse_y >= export_y && mouse_y <= export_y + export_height); //--- Checking if export button hovered
   if(is_export_hovered != prev_export_hovered) {     //--- Checking hover change
      ObjectSetInteger(0, PREFIX + EXPORT_BUTTON, OBJPROP_COLOR, is_export_hovered ? clrOrange : clrBlack); //--- Updating export button color
      ObjectSetInteger(0, PREFIX + EXPORT_BUTTON, OBJPROP_BGCOLOR, is_export_hovered ? clrDodgerBlue : clrNONE); //--- Updating export button background
      prev_export_hovered = is_export_hovered;        //--- Updating previous hover state
      ChartRedraw(0);                                 //--- Redrawing chart
   }
   int toggle_x = (int)ObjectGetInteger(0, PREFIX + TOGGLE_BUTTON, OBJPROP_XDISTANCE);    //--- Getting toggle button x-coordinate
   int toggle_y = (int)ObjectGetInteger(0, PREFIX + TOGGLE_BUTTON, OBJPROP_YDISTANCE);    //--- Getting toggle button y-coordinate
   int toggle_width = 20;                             //--- Setting toggle button width
   int toggle_height = 20;                            //--- Setting toggle button height
   bool is_toggle_hovered = (mouse_x >= toggle_x && mouse_x <= toggle_x + toggle_width &&
                             mouse_y >= toggle_y && mouse_y <= toggle_y + toggle_height); //--- Checking if toggle button hovered
   if(is_toggle_hovered != prev_toggle_hovered) {     //--- Checking hover change
      ObjectSetInteger(0, PREFIX + TOGGLE_BUTTON, OBJPROP_COLOR, is_toggle_hovered ? clrBlue : clrBlack); //--- Updating toggle button color
      ObjectSetInteger(0, PREFIX + TOGGLE_BUTTON, OBJPROP_BGCOLOR, is_toggle_hovered ? clrDodgerBlue : clrNONE); //--- Updating toggle button background
      prev_toggle_hovered = is_toggle_hovered;        //--- Updating previous hover state
      ChartRedraw(0);                                 //--- Redrawing chart
   }
}

//+------------------------------------------------------------------+
//| Updating header hover state                                      |
//+------------------------------------------------------------------+
void updateHeaderHoverState(int mouse_x, int mouse_y) {
   int header_x = (int)ObjectGetInteger(0, PREFIX + HEADER_PANEL, OBJPROP_XDISTANCE);  //--- Getting header x-coordinate
   int header_y = (int)ObjectGetInteger(0, PREFIX + HEADER_PANEL, OBJPROP_YDISTANCE);  //--- Getting header y-coordinate
   int header_width = (int)ObjectGetInteger(0, PREFIX + HEADER_PANEL, OBJPROP_XSIZE);  //--- Getting header width
   int header_height = (int)ObjectGetInteger(0, PREFIX + HEADER_PANEL, OBJPROP_YSIZE); //--- Getting header height
   int close_x = (int)ObjectGetInteger(0, PREFIX + CLOSE_BUTTON, OBJPROP_XDISTANCE);   //--- Getting close button x-coordinate
   int close_y = (int)ObjectGetInteger(0, PREFIX + CLOSE_BUTTON, OBJPROP_YDISTANCE);   //--- Getting close button y-coordinate
   int close_width = 20;                              //--- Setting close button width
   int close_height = 20;                             //--- Setting close button height
   int export_x = (int)ObjectGetInteger(0, PREFIX + EXPORT_BUTTON, OBJPROP_XDISTANCE); //--- Getting export button x-coordinate
   int export_y = (int)ObjectGetInteger(0, PREFIX + EXPORT_BUTTON, OBJPROP_YDISTANCE); //--- Getting export button y-coordinate
   int export_width = 20;                             //--- Setting export button width
   int export_height = 20;                            //--- Setting export button height
   int toggle_x = (int)ObjectGetInteger(0, PREFIX + TOGGLE_BUTTON, OBJPROP_XDISTANCE); //--- Getting toggle button x-coordinate
   int toggle_y = (int)ObjectGetInteger(0, PREFIX + TOGGLE_BUTTON, OBJPROP_YDISTANCE); //--- Getting toggle button y-coordinate
   int toggle_width = 20;                             //--- Setting toggle button width
   int toggle_height = 20;                            //--- Setting toggle button height
   bool is_header_hovered = (mouse_x >= header_x && mouse_x <= header_x + header_width &&
                             mouse_y >= header_y && mouse_y <= header_y + header_height &&
                             !(mouse_x >= close_x && mouse_x <= close_x + close_width &&
                               mouse_y >= close_y && mouse_y <= close_y + close_height) &&
                             !(mouse_x >= export_x && mouse_x <= export_x + export_width &&
                               mouse_y >= export_y && mouse_y <= export_y + export_height) &&
                             !(mouse_x >= toggle_x && mouse_x <= toggle_x + toggle_width &&
                               mouse_y >= toggle_y && mouse_y <= toggle_y + toggle_height)); //--- Checking if header hovered
   if(is_header_hovered != prev_header_hovered && !panel_dragging) {                         //--- Checking hover change
      ObjectSetInteger(0, PREFIX + HEADER_PANEL, OBJPROP_BGCOLOR, is_header_hovered ? clrRed : settings.section_bg_color); //--- Updating header background
      prev_header_hovered = is_header_hovered;        //--- Updating previous hover state
      ChartRedraw(0);                                 //--- Redrawing chart
   }
   updateButtonHoverStates(mouse_x, mouse_y);         //--- Updating button hover states
}

最後に、updateButtonHoverStatesおよびupdateHeaderHoverState関数を実装し、ユーザー操作に対する視覚的フィードバックを追加します。これにより、ボタンやヘッダーの反応性が向上します。updateButtonHoverStatesでは、「PREFIX + CLOSE_BUTTON」、「PREFIX + EXPORT_BUTTON」、「PREFIX + TOGGLE_BUTTON」の座標をObjectGetIntegerでOBJPROP_XDISTANCEおよびOBJPROP_YDISTANCEを取得し、close_width、close_height、export_width、export_height、toggle_width、toggle_heightを20に設定して、ボタンのホバー状態をチェックします。

閉じるボタンでは、mouse_xとmouse_yが範囲内にある場合にis_close_hoveredを設定し、prev_close_hoveredと異なる場合はObjectSetIntegerでOBJPROP_COLORをclrRedまたはclrBlack、OBJPROP_BGCOLORをclrDodgerBlueまたはclrNONEに更新し、prev_close_hoveredを更新してChartRedrawを呼び出します。エクスポートボタンでは、clrOrangeまたはclrBlack、clrDodgerBlueまたはclrNONEを設定してprev_export_hoveredを更新し、再描画します。トグルボタンではclrBlueまたはclrBlackを設定し、prev_toggle_hoveredを更新して再描画します。

updateHeaderHoverStateでは、「PREFIX + HEADER_PANEL」のheader_x、header_y、header_width、header_heightおよびボタン座標を取得し、カーソルがヘッダー内かつボタン範囲外にある場合にis_header_hoveredを設定します。is_header_hoveredがprev_header_hoveredと異なり、panel_draggingがfalseの場合は、ObjectSetIntegerで「PREFIX + HEADER_PANEL」のOBJPROP_BGCOLORをclrRedまたはsettings.section_bg_colorに更新し、prev_header_hoveredを更新してChartRedrawを呼び出し、さらにupdateButtonHoverStatesを実行します。これらの関数により、直感的なユーザー操作のための動的なホバーエフェクトが提供されます。これらの機能を活用するために、OnChartEvent関数を拡張して視覚的フィードバックのロジックを組み込みます。

//+------------------------------------------------------------------+
//| Handling chart events for sorting, export, and UI interactions   |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {
   if(id == CHARTEVENT_OBJECT_CLICK) {                //--- Handling object click
      if(sparam == PREFIX + CLOSE_BUTTON) {           //--- Checking close button click
         Print("Closing the dashboard");              //--- Logging closing
         PlaySound("alert.wav");                      //--- Playing alert sound
         panel_is_visible = false;                    //--- Setting panel invisible
         deleteAllObjects();                          //--- Deleting all objects
         ChartRedraw(0);                              //--- Redrawing chart
      }
      else if(sparam == PREFIX + EXPORT_BUTTON) {     //--- Checking export button click
         Print("Exporting dashboard to CSV");         //--- Logging exporting
         ExportToCSV();                               //--- Exporting to CSV
         ChartRedraw(0);                              //--- Redrawing chart
      }
      else if(sparam == PREFIX + TOGGLE_BUTTON) {     //--- Checking toggle button click
         deleteAllObjects();                          //--- Deleting all objects
         panel_minimized = !panel_minimized;          //--- Toggling minimized state
         if(panel_minimized) {                        //--- Checking if minimized
            Print("Minimizing the dashboard");        //--- Logging minimizing
            createMinimizedDashboard();               //--- Creating minimized dashboard
         } else {
            Print("Maximizing the dashboard");        //--- Logging maximizing
            createFullDashboard();                    //--- Creating full dashboard
            // Resetting string variables to force update
            total_buys_str = "";                      //--- Resetting buys string
            total_sells_str = "";                     //--- Resetting sells string
            total_trades_str = "";                    //--- Resetting trades string
            total_lots_str = "";                      //--- Resetting lots string
            total_profit_str = "";                    //--- Resetting profit string
            total_pending_str = "";                   //--- Resetting pending string
            total_swap_str = "";                      //--- Resetting swap string
            total_comm_str = "";                      //--- Resetting commission string
            acc_bal_str = "";                         //--- Resetting balance string
            acc_eq_str = "";                          //--- Resetting equity string
            acc_free_str = "";                        //--- Resetting free margin string
            UpdateDashboard();                        //--- Updating dashboard
         }
         prev_header_hovered = false;                 //--- Resetting header hover
         prev_close_hovered = false;                  //--- Resetting close hover
         prev_export_hovered = false;                 //--- Resetting export hover
         prev_toggle_hovered = false;                 //--- Resetting toggle hover
         ObjectSetInteger(0, PREFIX + HEADER_PANEL, OBJPROP_BGCOLOR, settings.section_bg_color); //--- Resetting header background
         ObjectSetInteger(0, PREFIX + CLOSE_BUTTON, OBJPROP_COLOR, clrBlack); //--- Resetting close button color
         ObjectSetInteger(0, PREFIX + CLOSE_BUTTON, OBJPROP_BGCOLOR, clrNONE); //--- Resetting close button background
         ObjectSetInteger(0, PREFIX + EXPORT_BUTTON, OBJPROP_COLOR, clrBlack); //--- Resetting export button color
         ObjectSetInteger(0, PREFIX + EXPORT_BUTTON, OBJPROP_BGCOLOR, clrNONE); //--- Resetting export button background
         ObjectSetInteger(0, PREFIX + TOGGLE_BUTTON, OBJPROP_COLOR, clrBlack); //--- Resetting toggle button color
         ObjectSetInteger(0, PREFIX + TOGGLE_BUTTON, OBJPROP_BGCOLOR, clrNONE); //--- Resetting toggle button background
         ChartRedraw(0);                              //--- Redrawing chart
      }
      else {
         for(int i = 0; i < ArraySize(headers); i++) { //--- Iterating through headers
            if(sparam == PREFIX + HEADER + IntegerToString(i)) { //--- Checking header click
               if(sort_column == i)                   //--- Checking if same column
                  sort_ascending = !sort_ascending;   //--- Toggling sort direction
               else {
                  sort_column = i;                    //--- Setting new sort column
                  sort_ascending = true;              //--- Setting to ascending
               }
               UpdateDashboard();                     //--- Updating dashboard
               break;                                 //--- Exiting loop
            }
         }
      }
   }
   else if(id == CHARTEVENT_KEYDOWN && lparam == 'E') { //--- Handling 'E' key press
      ExportToCSV();                                  //--- Exporting to CSV
   }
   else if(id == CHARTEVENT_MOUSE_MOVE && panel_is_visible) { //--- Handling mouse move
      int mouse_x = (int)lparam;                      //--- Getting mouse x-coordinate
      int mouse_y = (int)dparam;                      //--- Getting mouse y-coordinate
      int mouse_state = (int)sparam;                  //--- Getting mouse state
      if(mouse_x == last_mouse_x && mouse_y == last_mouse_y && !panel_dragging) { //--- Checking if mouse moved
         return;                                      //--- Exiting if no movement
      }
      last_mouse_x = mouse_x;                         //--- Updating last mouse x
      last_mouse_y = mouse_y;                         //--- Updating last mouse y
      updateHeaderHoverState(mouse_x, mouse_y);       //--- Updating header hover state
      int header_x = (int)ObjectGetInteger(0, PREFIX + HEADER_PANEL, OBJPROP_XDISTANCE); //--- Getting header x-coordinate
      int header_y = (int)ObjectGetInteger(0, PREFIX + HEADER_PANEL, OBJPROP_YDISTANCE); //--- Getting header y-coordinate
      int header_width = (int)ObjectGetInteger(0, PREFIX + HEADER_PANEL, OBJPROP_XSIZE); //--- Getting header width
      int header_height = (int)ObjectGetInteger(0, PREFIX + HEADER_PANEL, OBJPROP_YSIZE);//--- Getting header height
      int close_x = (int)ObjectGetInteger(0, PREFIX + CLOSE_BUTTON, OBJPROP_XDISTANCE);  //--- Getting close button x-coordinate
      int close_width = 20;                           //--- Setting close button width
      int export_x = (int)ObjectGetInteger(0, PREFIX + EXPORT_BUTTON, OBJPROP_XDISTANCE);//--- Getting export button x-coordinate
      int export_width = 20;                          //--- Setting export button width
      int toggle_x = (int)ObjectGetInteger(0, PREFIX + TOGGLE_BUTTON, OBJPROP_XDISTANCE);//--- Getting toggle button x-coordinate
      int toggle_width = 20;                          //--- Setting toggle button width
      if(prev_mouse_state == 0 && mouse_state == 1) { //--- Checking mouse click start
         if(mouse_x >= header_x && mouse_x <= header_x + header_width &&
            mouse_y >= header_y && mouse_y <= header_y + header_height &&
            !(mouse_x >= close_x && mouse_x <= close_x + close_width) &&
            !(mouse_x >= export_x && mouse_x <= export_x + export_width) &&
            !(mouse_x >= toggle_x && mouse_x <= toggle_x + toggle_width)) { //--- Checking if in draggable area
            panel_dragging = true;                     //--- Starting dragging
            panel_drag_x = mouse_x;                    //--- Setting drag start x
            panel_drag_y = mouse_y;                    //--- Setting drag start y
            panel_start_x = header_x;                  //--- Setting panel start x
            panel_start_y = header_y;                  //--- Setting panel start y
            ObjectSetInteger(0, PREFIX + HEADER_PANEL, OBJPROP_BGCOLOR, clrMediumBlue); //--- Setting dragging color
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false); //--- Disabling chart scroll
         }
      }
      if(panel_dragging && mouse_state == 1) {        //--- Handling dragging
         int dx = mouse_x - panel_drag_x;             //--- Calculating x change
         int dy = mouse_y - panel_drag_y;             //--- Calculating y change
         settings.panel_x = panel_start_x + dx;       //--- Updating panel x
         settings.panel_y = panel_start_y + dy;       //--- Updating panel y
         int chart_width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);   //--- Getting chart width
         int chart_height = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Getting chart height
         int num_columns = ArraySize(headers);        //--- Getting number of columns
         int column_width_sum = 0;                    //--- Initializing column width sum
         for(int i = 0; i < num_columns; i++) column_width_sum += column_widths[i]; //--- Adding column width
         int panel_width = MathMax(settings.header_x_distances[num_columns - 1] + column_widths[num_columns - 1], column_width_sum) + 20 + settings.label_x_offset; //--- Calculating panel width
         int panel_height = panel_minimized ? settings.row_height : (ArraySize(symbol_data) + 3) * settings.row_height; //--- Calculating panel height
         settings.panel_x = MathMax(0, MathMin(chart_width - panel_width, settings.panel_x)); //--- Constraining x
         settings.panel_y = MathMax(0, MathMin(chart_height - panel_height, settings.panel_y)); //--- Constraining y
         updatePanelPositions();                      //--- Updating object positions
         ChartRedraw(0);                              //--- Redrawing chart
      }
      if(mouse_state == 0 && prev_mouse_state == 1) { //--- Handling mouse release
         if(panel_dragging) {                         //--- Checking if was dragging
            panel_dragging = false;                   //--- Stopping dragging
            updateHeaderHoverState(mouse_x, mouse_y); //--- Updating hover state
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enabling chart scroll
            ChartRedraw(0);                           //--- Redrawing chart
         }
      }
      prev_mouse_state = mouse_state;                //--- Updating previous mouse state
   }
}

ここでは、OnChartEvent関数を拡張してインタラクティブなイベントを処理できるようにします。これにより、クローズ、エクスポート、トグル、ソートのクリックや、ドラッグのためのマウス移動を管理します。CHARTEVENT_OBJECT_CLICKの場合、sparamが「PREFIX + CLOSE_BUTTON」であれば、Printでログを出力し、PlaySoundでalert.wavを再生し、panel_is_visibleをfalseに設定してdeleteAllObjectsを呼び出し、ChartRedrawで再描画します。sparamが「PREFIX + EXPORT_BUTTON」の場合は、ログ出力後にExportToCSVを呼び出します。

「PREFIX + TOGGLE_BUTTON」の場合は、オブジェクトを削除し、panel_minimizedを切り替え、Printで「Minimizing」または「Maximizing」を出力し、createMinimizedDashboardまたはcreateFullDashboardを呼び出します。さらに、total_buys_strやacc_bal_strなどの文字列変数をリセットし、UpdateDashboardを呼び出します。ホバー状態(prev_header_hovered、prev_close_hoveredなど)をリセットし、「PREFIX + HEADER_PANEL」、「PREFIX + CLOSE_BUTTON」、「PREFIX + EXPORT_BUTTON」、「PREFIX + TOGGLE_BUTTON」の色もObjectSetIntegerでリセットします。これにより、次のような動作を実現できます。

最小化された状態

ヘッダーのクリックについては、headersをループし、sort_columnが一致する場合はsort_ascendingを切り替え、そうでなければ新しいsort_columnを設定してsort_ascendingをtrueにし、UpdateDashboardを呼び出します。CHARTEVENT_KEYDOWNで「E」が押された場合はExportToCSVを呼び出します。CHARTEVENT_MOUSE_MOVEでは、panel_is_visibleがtrueの場合、mouse_x、mouse_y、mouse_stateを取得し、状態が変わっておらずドラッグもおこなわれていなければ処理を終了します。last_mouse_xとlast_mouse_yを更新し、updateHeaderHoverStateを呼び出します。

prev_mouse_stateが0でmouse_stateが1の場合は、ボタンを除くドラッグ可能領域のクリックをチェックし、panel_draggingをtrueに設定して座標を保存し、ヘッダーの色をclrMediumBlueに変更、ChartSetIntegerでスクロールを無効化します。ドラッグ中かつmouse_stateが1の場合はdxとdyを計算し、settings.panel_xとsettings.panel_yをチャート範囲内で更新し、updatePanelPositionsを呼び出して再描画します。マウスを離した際にはドラッグを終了し、ホバー状態を更新、スクロールを再有効化して再描画します。これにより、ユーザーフレンドリーなダッシュボードの動的なUI操作が可能になります。コンパイル後には、ボタンのホバー状態が確認できます。

ボタンのホバー状態

最大化状態およびドラッグ状態では、次のような結果となります。

最終ドラッグ状態

画像からも、ホバー、ドラッグ、最小化のロジックに対応したダッシュボードコンポーネントが追加され、目的どおりに動作していることが確認できます。残されているのはプロジェクトの実用性をテストすることであり、それは次のセクションで扱います。


バックテスト

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

バックテスト


結論

結論として、第8回ではMQL5の情報ダッシュボードを拡張し、ドラッグ&最小化機能、CLOSE_BUTTONやTOGGLE_BUTTONなどのインタラクティブボタン、さらにホバーエフェクトを追加しました。これにより、複数銘柄のポジションや口座の監視機能を維持しつつ、ユーザー体験が向上しています。アーキテクチャと実装手順も詳細に説明し、createFullDashboardやupdatePanelPositions、OnChartEventなどの関数を用いて、柔軟で視覚的に反応するツールを実現しました。リアルタイム更新やExcel CSVへのエクスポート機能も備えており、取引ワークフローの最適化やポジション分析の直感的かつ効率的な運用が可能です。

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

添付されたファイル |
取引システムの構築(第2回):ポジションサイズ管理の科学 取引システムの構築(第2回):ポジションサイズ管理の科学
期待値がプラスのシステムであっても、ポジションサイズ管理の決定次第で取引が成功するか破綻するかが決まります。ポジションサイズ管理はリスク管理の中心であり、統計的な優位性を現実の利益に変換しつつ、資本を守る役割を担います。
共和分株式による統計的裁定取引(第2回):エキスパートアドバイザー、バックテスト、最適化 共和分株式による統計的裁定取引(第2回):エキスパートアドバイザー、バックテスト、最適化
この記事では、ナスダックの4銘柄のバスケットを対象としたサンプルのエキスパートアドバイザー(EA)実装を紹介します。銘柄はまずピアソン相関係数に基づいてフィルタリングされました。その後、フィルタリングされた銘柄群について、ジョハンセン検定を用いて共和分関係の有無を検証しました。最後に、共和分関係から得られたスプレッドについて、ADF検定およびKPSS検定を用いて定常性を検証しました。ここでは、このプロセスに関する補足と、小規模な最適化後のバックテスト結果について説明します。
ダイナミックマルチペアEAの形成(第4回):ボラティリティとリスク調整 ダイナミックマルチペアEAの形成(第4回):ボラティリティとリスク調整
このフェーズでは、マルチペアEAを微調整し、ATRなどのボラティリティ指標を活用してリアルタイムで取引サイズとリスクを調整します。これにより、一貫性の向上、資金保護、そしてさまざまな市場状況下でのパフォーマンス改善を実現します。
ログレコードをマスターする(第10回):抑制機能を実装してログの再表示を防ぐ ログレコードをマスターする(第10回):抑制機能を実装してログの再表示を防ぐ
Logifyライブラリにおけるログ抑制システムを作成しました。本記事では、CLogifySuppressionクラスがどのようにコンソールのノイズを低減するかについて詳しく説明します。このクラスは、繰り返しや無関係なメッセージを回避するための設定可能なルールを適用します。また、外部設定フレームワーク、検証機構、包括的なテストについても取り上げ、ボットやインジケーター開発時のログ取得における堅牢性と柔軟性を確保しています。