English Русский Deutsch 日本語
preview
MQL5交易工具(第八部分):具备可拖拽与可最小化功能的增强型信息仪表盘

MQL5交易工具(第八部分):具备可拖拽与可最小化功能的增强型信息仪表盘

MetaTrader 5交易 |
23 0
Allan Munene Mutiiria
Allan Munene Mutiiria

引言

在前一篇文章(第七部分)中,我们使用MetaQuotes Language 5(MQL5)开发了一款信息仪表盘,用于监控多品种持仓以及账户指标,例如“账户余额”、“净值”、“可用保证金”,同时具备可排序列与逗号分隔值(CSV)导出功能。在第八部分,我们对该仪表盘进行功能增强:新增可拖拽、可最小化特性,增加用于关闭、切换按钮和导出的交互按钮,同时加入鼠标悬停效果,带来更具动态的交互体验。本次升级依旧保留了实时持仓追踪以及表头发光特效。我们将涵盖以下主题:

  1. 理解增强版仪表盘的架构
  2. 在MQL5中的实现
  3. 回测
  4. 结论

到本文结束时,您将得到一个功能全面、易于使用的MQL5仪表盘,专门用于高效监控交易情况 —— 让我们正式开始!


理解增强版仪表盘的架构

我们将对第七部分的信息仪表盘进行升级,新增可拖拽与可最小化功能,同时加入交互按钮以及鼠标悬停效果,让该工具在管理多笔持仓时更加灵活、更便于使用。本次升级具备实际意义:拖拽功能支持将仪表盘移动至图表任意位置,避免分析界面杂乱;最小化选项可以减少屏幕占用;各类交互组件能够提供即时视觉反馈,在节奏快速的交易环境下,整体使用体验得以提升。

为实现以上效果,我们将加入用于拖拽、按钮点击的鼠标事件处理逻辑,保证仪表盘响应灵敏、适配性强,同时不丢失核心的持仓跟踪能力。我们还会在表头位置增设导出功能图标,方便用户快速找到该功能,同时保留键盘快捷键的操作方式。在新增这些增强特性的同时,我们将继续保留可排序表格与实时刷新能力,打造一款日常使用时直观又高效的工具。请先看我们预期实现的效果,之后再进入具体代码实现环节!

实施计划


在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

我们首先新增宏定义(defines),用于支撑信息仪表盘的各项增强功能,为交互式用户界面(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交互效果。现在变量已经更新完毕,接下来我们同样对函数进行更新,对于对象创建逻辑做标准化处理,这样有助于进一步提高模块化程度。我们从创建标签的函数开始,为其新增工具提示 (tooltip)功能。

//+------------------------------------------------------------------+
//| 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"函数,我们对tooltip逻辑做优化,使其更加灵活,能够复用于各类UI控件。此前工具提示属于硬编码实现:通过ObjectSetString将属性OBJPROP_TOOLTIP设置固定文本,可点击标签写死为“点击进行排序”,不可点击标签写死为“持仓数据”,自定义能力受限。本次修改新增一个"tooltip"参数,默认值为空字符串;并在"ObjectSetString"设置"OBJPROP_TOOLTIP"属性处使用三元条件判断:如果传入的 `tooltip` 参数为空,可点击标签默认显示 “点击进行排序”,其余标签默认显示 “持仓数据”;如果参数不为空,则直接使用传入的"tooltip"值。该改动可以为按钮类控件设置专属提示文字(例如“最小化仪表盘”或“关闭仪表盘”),同时保留表头、数据标签的默认提示。以此优化用户指引,提升交互的清晰度。

接下来,为了标准化面板创建逻辑,我们将把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
}

这里,我们仅需创建布尔返回值类型的"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中通常更为安全,当然您也可以选择保留原来的方式。除此之外,在选取订单与持仓之前增加了订单单号校验,避免迭代过程中因无效单号或列表发生改动而产生潜在错误。由于我们需要完整的动态效果,我们把仪表盘的初始化创建逻辑封装到独立函数中,并且拆分为两个函数:一个用于面板最大化状态下的创建,另一个用于面板最小化状态下的创建。

//+------------------------------------------------------------------+
//| 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
}

我们实现"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)。全部按钮均设置专属工具提示,同时开启可点击属性(selectable = true)。图标样式可由您自行选择。下面给出一套可直接使用的字体精简示例,只需使用正确的符号与字符即可。

符号字体

接下来,我们在计算得出的坐标位置创建表头标签集合"headers";创建品种标签,内容取自"symbol_data[i].name";创建带初始值的数据标签;随后创建页脚面板"PREFIX + FOOTER_PANEL"、页脚文本“Total:”以及页脚数据标签;再创建账户面板(对象名"PREFIX + ACCOUNT_PANEL"),并为"account_items"创建各项账户信息标签。以上全部通过"createRectangle"与"createLABEL"完成,配置合适的坐标与颜色,最后调用ChartRedraw函数刷新图表。

在"createMinimizedDashboard"函数中,构建精简界面:仅保留表头面板,通过"createRectangle"创建"PREFIX + HEADER_PANEL";调用"createLABEL"生成"PREFIX + HEADER_PANEL_TEXT";同时创建导出按钮(Wingdings 字体,字符代码60)、切换按钮(Wingdings字体,字符o,用于最大化)、关闭按钮(Webdings字体,字符r),最大限度节省屏幕占用,完成后执行重绘。

"deleteAllObjects"函数借助ObjectsDeleteAll,配合对象名前缀"PREFIX",删除全部图表上所有类型的仪表盘相关对象,保证界面更新或者关闭时环境干净无残留。以上函数使得仪表盘可以灵活在完整模式、最小化模式之间切换,同时支持拖拽、面板状态切换等用户交互。现在,我们就使用这套动态函数,对仪表盘主逻辑函数进行更新。

//+------------------------------------------------------------------+
//| 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"创建完整用户界面;使用ArraySize获取"symbol_data"数组的大小并赋值给"prev_num_symbols",记录初始的持仓品种数量;通过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"求和,再结合MathMax函数、"settings.header_x_distances"以及边距,计算得到面板宽度 "panel_width"。通过ObjectSetInteger,配合"settings.panel_x"、"settings.panel_y"坐标,为以下对象设置OBJPROP_XDISTANCE和"OBJPROP_YDISTANCE"属性,完成表头面板与按钮的位置更新:"PREFIX + HEADER_PANEL"、"PREFIX + HEADER_PANEL_TEXT"、"PREFIX + EXPORT_BUTTON"、"PREFIX + TOGGLE_BUTTON"和"PREFIX + CLOSE_BUTTON"。

如果"panel_minimized"为false(面板处于完整展开状态):更新主面板"PREFIX + PANEL"的位置;根据"settings.panel_y + settings.row_height + 8 + settings.label_y_offset"计算得到"header_y",更新表头;基于"first_row_y"、"symbol_x",配合经过"column_widths"调整后的"x_offset"更新品种标签与数据标签;根据"num_rows + 3"计算出"footer_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";再通过变量"in_header"判断鼠标坐标("mouse_x"、"mouse_y")是否处于表头的矩形范围内。

用同样方式读取"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"函数内,我们调用ObjectGetInteger获取"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。

针对关闭按钮,判断鼠标坐标"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"打印 “正在最小化”或“正在还原最大化”日志;调用"createMinimizedDashboard"或"createFullDashboard";重置"total_buys_str"和"acc_bal_str"等字符串变量;调用"UpdateDashboard"刷新仪表盘;重置各类悬停状态变量("prev_header_hovered"、"prev_close_hovered"等);再使用ObjectSetInteger函数重置"PREFIX + HEADER_PANEL"、"PREFIX + CLOSE_BUTTON"、"PREFIX + EXPORT_BUTTON"和"PREFIX + TOGGLE_BUTTON"的颜色属性。执行后将得到如下效果。

最小化状态

针对表头点击事件,遍历表头数组"headers";若点击列与当前"sort_column"一致,则翻转"sort_ascending"排序标识;否则设置新的"sort_column",并将"sort_ascending"设置为true,随后调用"UpdateDashboard"刷新面板。检测到CHARTEVENT_KEYDOWN键盘按下事件,按键为E时,调用"ExportToCSV"执行导出。当仪表盘处于可见状态,触发CHARTEVENT_MOUSE_MOVE鼠标移动事件时:获取鼠标坐标"mouse_x"、"mouse_y"以及鼠标状态;若鼠标位置无变化且未开启拖拽则直接退出;更新"last_mouse_x"、"last_mouse_y",并调用"updateHeaderHoverState"更新表头悬停状态。

如果"prev_mouse_state"为0(鼠标未按下)且当前"mouse_state"为 1(鼠标左键按下),判断点击发生在可拖拽区域(排除按钮区域);将"panel_dragging"设置为true,保存鼠标起始坐标;把表头颜色设置为"clrMediumBlue",并调用ChartSetInteger函数禁用图表滚动。拖拽过程中且鼠标左键保持按下状态:计算鼠标偏移量"dx"和"dy",在图表边界范围内更新面板坐标"settings.panel_x"与"settings.panel_y";调用"updatePanelPositions"更新面板位置,执行图表重绘。鼠标松开时,结束拖拽状态,更新悬停状态,重新开启图表滚动,并重绘界面。以上逻辑为仪表盘实现了流畅的动态交互,提升使用体验。编译完成后,即可看到下述按钮悬停效果。

按钮悬停状态

面板最大化且处于拖拽状态下的运行效果如下。

最终拖拽状态

由图可见,我们已经完成仪表盘各项组件的开发,实现了鼠标悬浮、拖拽以及面板最小化相关逻辑,达成了开发目标。当前仅需完成项目可操作性的测试工作,该部分内容已在前文章节中详细阐述。


回测

我们已完成测试,以下是编译后得到的可视化结果,以单一的GIF动图形式呈现。

回测


结论

综上所述,我们完成了MQL5第八部分信息仪表盘的功能增强:新增可拖拽、可最小化特性,实现了如"CLOSE_BUTTON"和"TOGGLE_BUTTON"等交互按钮以及悬浮高亮效果,在保障多品种持仓与账户状态稳定监控的同时,提升了用户使用体验。本文详细讲解了整体架构与代码实现,借助"createFullDashboard"、"updatePanelPositions"以及OnChartEvent等函数,打造出一款灵活、视觉反馈灵敏的工具,支持实时数据刷新与Excel兼容的CSV文件导出。您可以对该仪表盘进行自定义修改,优化自身交易工作流,让持仓分析更加直观高效。

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/19059

附加的文件 |
交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
基于协整股票的统计套利(第二部分):EA、回测与优化 基于协整股票的统计套利(第二部分):EA、回测与优化
本文展示一个智能交易系统(EA)示例,用于交易一篮子四只纳斯达克股票。选股流程如下:首先通过皮尔逊相关系数检验进行初步筛选;再对筛选后的股票组进行约翰森协整检验;最后通过ADF检验与KPSS检验验证协整价差的平稳性。本文将对这一流程做补充说明,并展示经过小幅优化后的回测结果。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
Python-MetaTrader 5 策略测试器(第一部分):交易模拟器 Python-MetaTrader 5 策略测试器(第一部分):交易模拟器
Python 提供的 MetaTrader 5 模块支持通过 Python 在 MetaTrader 5 客户端发起交易,但存在一个重大缺陷:该模块不具备 MetaTrader 5 客户端自带的策略测试器功能。在本系列文章中,我们将搭建一套框架,实现基于 Python 环境的交易策略回测。