English Русский Deutsch 日本語
preview
MQL5交易工具(第四部分):为多周期扫描仪表盘添加动态定位与切换功能

MQL5交易工具(第四部分):为多周期扫描仪表盘添加动态定位与切换功能

MetaTrader 5交易 |
30 2
Allan Munene Mutiiria
Allan Munene Mutiiria

概述

在本系列先前的文章(第三部分)中,我们使用MetaQuotes Language 5(MQL5)开发了一款多周期扫描仪表盘,该工具可以同时显示当前交易品种在多个时间框架下的相对强弱指数(RSI)、随机指标(Stochastic)、商品通道指数(CCI)、平均趋向指数(ADX)及动量震荡指标(AO),通过跨周期指标联动分析捕捉交易信号。本文(第四部分)将对该仪表盘进行功能升级,新增动态定位系统,支持在图表上任意位置拖拽仪表盘,并新增切换功能以最小化或最大化显示,从而提升可用性和屏幕管理效率。我们将涵盖以下主题:

  1. 理解动态定位与切换功能架构
  2. 在MQL5中的实现
  3. 回测
  4. 结论

阅读完全文,您将掌握如何构建一个具备灵活定位和切换功能的高级MQL5仪表盘,并可对其进行测试与定制。话不多说,让我们开始!


理解动态定位与切换功能架构

我们通过添加动态定位功能来优化多时间周期扫描仪表盘,允许在图表上拖动,并提供最小化或最大化切换,从而提升可用性。这些功能对于避免图表杂乱、优化屏幕空间以进行高效交易分析至关重要。我们将实现鼠标驱动的拖动以重新定位仪表盘,以及切换按钮在紧凑视图和完整视图之间切换,同时保持指标无缝更新,以支持更好的交易决策。简单来说,目标实现效果如下所示。

定位与拖动状态架构


在MQL5中的实现

为了在MQL5中实现这些优化功能,我们将定义一个额外的切换按钮对象,后续用于在最大化和最小化状态之间切换,而悬停和拖拽操作将基于已实现的标题栏对象完成。

// Define identifiers and properties for UI elements
#define MAIN_PANEL              "PANEL_MAIN"                     //--- Main panel rectangle identifier

//--- THE REST OF THE EXISTING OBJECTS

#define TOGGLE_BUTTON           "BUTTON_TOGGLE"                  //--- Toggle (minimize/maximize) button identifier

//--- THE REST OF THE EXISTING OBJECTS

#define COLOR_DARK_GRAY         C'105,105,105'                   //--- Dark gray color for indicator backgrounds

我们开始对多周期扫描仪表盘进行功能优化,首先更新用户界面(UI)元素定义,为切换按钮新增标识符,以支持添加最小化/最大化功能的目标。保留现有定义以维持仪表盘的核心结构不变。关键性改动是新增"TOGGLE_BUTTON"标识符(定义为"BUTTON_TOGGLE"),将用于创建切换仪表盘在最小化与最大化状态间转换的按钮。

这一点至关重要,它开启了全新的切换功能,用户可以折叠仪表盘节省空间,或展开查看全景,在不改动现有指标逻辑的同时提升了可用性。接下来,我们需要进一步控制全局变量,这些变量将用于存储仪表盘的当前状态。

bool panel_minimized = false;                                    //--- Flag to control minimized state
int panel_x = 632, panel_y = 40;                                 //--- Panel position coordinates
bool panel_dragging = false;                                     //--- Flag to track if panel is being dragged
int panel_drag_x = 0, panel_drag_y = 0;                          //--- Mouse coordinates when drag starts
int panel_start_x = 0, panel_start_y = 0;                        //--- Panel coordinates when drag starts
int prev_mouse_state = 0;                                        //--- Variable to track 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
int last_mouse_x = 0, last_mouse_y = 0;                          //--- Track last mouse position for optimization
bool prev_header_hovered = false;                                //--- Track previous header hover state
bool prev_toggle_hovered = false;                                //--- Track previous toggle hover state
bool prev_close_hovered = false;                                 //--- Track previous close button hover state

为了追踪仪表盘状态,我们新增部分全局变量。引入"panel_minimized"记录最小化状态,"panel_x"和"panel_y"存储仪表盘位置坐标,"panel_dragging"、"panel_drag_x"、"panel_drag_y"、"panel_start_x"和"panel_start_y"用于拖拽操作。添加"prev_mouse_state"、"header_hovered"、"toggle_hovered"、"close_hovered"、"last_mouse_x"、"last_mouse_y"、"prev_header_hovered"、"prev_toggle_hovered"、"prev_close_hovered"管理鼠标事件与悬停状态。这些变量将支持实现可拖拽、可交互且带切换功能的仪表盘。

接下来,我们开始将切换按钮添加至仪表盘。为管理状态切换,需要为最大化面板和最小化面板分别编写独立的创建逻辑函数。让我们先从最大化仪表盘的实现开始。

//+------------------------------------------------------------------+
//| Create full dashboard UI                                         |
//+------------------------------------------------------------------+
void create_full_dashboard() {
   create_rectangle(MAIN_PANEL, panel_x, panel_y, 617, 374, C'30,30,30', BORDER_FLAT); //--- Create main panel background
   create_rectangle(HEADER_PANEL, panel_x, panel_y, 617, 27, C'60,60,60', BORDER_FLAT); //--- Create header panel background
   create_label(HEADER_PANEL_ICON, CharToString(91), panel_x - 12, panel_y + 14, 18, clrAqua, "Wingdings"); //--- Create header icon
   create_label(HEADER_PANEL_TEXT, "TimeframeScanner", panel_x - 105, panel_y + 12, 13, COLOR_WHITE); //--- Create header title
   create_label(CLOSE_BUTTON, CharToString('r'), panel_x - 600, panel_y + 14, 18, clrYellow, "Webdings"); //--- Create close button
   create_label(TOGGLE_BUTTON, CharToString('r'), panel_x - 570, panel_y + 14, 18, clrYellow, "Wingdings"); //--- Create minimize button (-)

   // Create header rectangle and label
   create_rectangle(SYMBOL_RECTANGLE, panel_x - 2, panel_y + 35, WIDTH_TIMEFRAME, HEIGHT_RECTANGLE, clrGray); //--- Create symbol rectangle
   create_label(SYMBOL_TEXT, _Symbol, panel_x - 47, panel_y + 45, 11, COLOR_WHITE); //--- Create symbol label
   
   // Create summary and indicator headers (rectangles and labels)
   string header_names[] = {"BUY", "SELL", "RSI", "STOCH", "CCI", "ADX", "AO"}; //--- Define header titles
   for(int header_index = 0; header_index < ArraySize(header_names); header_index++) { //--- Loop through headers
      int x_offset = panel_x - WIDTH_TIMEFRAME - (header_index < 2 ? header_index * WIDTH_SIGNAL : 2 * WIDTH_SIGNAL + (header_index - 2) * WIDTH_INDICATOR) + (1 + header_index); //--- Calculate x position
      int width = (header_index < 2 ? WIDTH_SIGNAL : WIDTH_INDICATOR); //--- Set width based on header type
      create_rectangle(HEADER_RECTANGLE + IntegerToString(header_index), x_offset, panel_y + 35, width, HEIGHT_RECTANGLE, clrGray); //--- Create header rectangle
      create_label(HEADER_TEXT + IntegerToString(header_index), header_names[header_index], x_offset - width/2, panel_y + 45, 11, COLOR_WHITE); //--- Create header label
   }
   
   // Create timeframe rectangles and labels, and summary/indicator cells
   for(int timeframe_index = 0; timeframe_index < ArraySize(timeframes_array); timeframe_index++) { //--- Loop through timeframes
      // Highlight current timeframe
      color timeframe_background = (timeframes_array[timeframe_index] == _Period) ? clrLimeGreen : clrGray; //--- Set background color for current timeframe
      color timeframe_text_color = (timeframes_array[timeframe_index] == _Period) ? COLOR_BLACK : COLOR_WHITE; //--- Set text color for current timeframe
      
      create_rectangle(TIMEFRAME_RECTANGLE + IntegerToString(timeframe_index), panel_x - 2, (panel_y + 35 + HEIGHT_RECTANGLE) + timeframe_index * HEIGHT_RECTANGLE - (1 + timeframe_index), WIDTH_TIMEFRAME, HEIGHT_RECTANGLE, timeframe_background); //--- Create timeframe rectangle
      create_label(TIMEFRAME_TEXT + IntegerToString(timeframe_index), truncate_timeframe_name(timeframe_index), panel_x - 47, (panel_y + 45 + HEIGHT_RECTANGLE) + timeframe_index * HEIGHT_RECTANGLE - (1 + timeframe_index), 11, timeframe_text_color); //--- Create timeframe label
                  
      // Create summary and indicator cells
      for(int header_index = 0; header_index < ArraySize(header_names); header_index++) { //--- Loop through headers for cells
         string cell_rectangle_name, cell_text_name;              //--- Declare cell name and label variables
         color cell_background = (header_index < 2) ? COLOR_LIGHT_GRAY : COLOR_BLACK; //--- Set cell background color
         switch(header_index) {                                   //--- Select cell type
            case 0: cell_rectangle_name = BUY_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = BUY_TEXT + IntegerToString(timeframe_index); break; //--- Buy cell
            case 1: cell_rectangle_name = SELL_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = SELL_TEXT + IntegerToString(timeframe_index); break; //--- Sell cell
            case 2: cell_rectangle_name = RSI_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = RSI_TEXT + IntegerToString(timeframe_index); break; //--- RSI cell
            case 3: cell_rectangle_name = STOCH_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = STOCH_TEXT + IntegerToString(timeframe_index); break; //--- Stochastic cell
            case 4: cell_rectangle_name = CCI_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = CCI_TEXT + IntegerToString(timeframe_index); break; //--- CCI cell
            case 5: cell_rectangle_name = ADX_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = ADX_TEXT + IntegerToString(timeframe_index); break; //--- ADX cell
            case 6: cell_rectangle_name = AO_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = AO_TEXT + IntegerToString(timeframe_index); break; //--- AO cell
         }
         int x_offset = panel_x - WIDTH_TIMEFRAME - (header_index < 2 ? header_index * WIDTH_SIGNAL : 2 * WIDTH_SIGNAL + (header_index - 2) * WIDTH_INDICATOR) + (1 + header_index); //--- Calculate x position
         int width = (header_index < 2 ? WIDTH_SIGNAL : WIDTH_INDICATOR); //--- Set width based on cell type
         create_rectangle(cell_rectangle_name, x_offset, (panel_y + 35 + HEIGHT_RECTANGLE) + timeframe_index * HEIGHT_RECTANGLE - (1 + timeframe_index), width, HEIGHT_RECTANGLE, cell_background); //--- Create cell rectangle
         create_label(cell_text_name, "-/-", x_offset - width/2, (panel_y + 45 + HEIGHT_RECTANGLE) + timeframe_index * HEIGHT_RECTANGLE - (1 + timeframe_index), 10, COLOR_WHITE); //--- Create cell label
      }
   }
   ChartRedraw(0);
}

我们通过创建"create_full_dashboard"函数实现仪表盘状态管理,将原有的静态创建逻辑迁移至该函数,并更新为支持动态定位和切换功能。核心变更是将"panel_x"和"panel_y"全局变量集成到所有仪表盘元素的定位逻辑中,使仪表盘可放置在图表的任意位置。我们使用"create_rectangle"函数并传入"MAIN_PANEL"创建主面板,通过"panel_x"和"panel_y"确定位置,保持尺寸为617x374像素:同样地,我们使用"create_rectangle"和"create_label"定位标题面板、图标和标题,分别为"HEADER_PANEL"、"HEADER_PANEL_ICON"和"HEADER_PANEL_TEXT",并根据"panel_x"和"panel_y"调整它们的x和y坐标。

我们通过"create_label"函数新增切换按钮"TOGGLE_BUTTON",将其定位在"panel_x - 570"和"panel_y + 14"处,并使用Webdings字体中的缩小符号('r')作为按钮图标。符号背景矩形"SYMBOL_RECTANGLE"和图标文本"SYMBOL_TEXT",以及标题栏背景矩形"HEADER_RECTANGLE"和标题文本"HEADER_TEXT"的坐标全部基于"panel_x"和"panel_y"偏移,确保它们随面板整体移动。对"timeframes_array"中的每个时间周期,我们创建时间周期矩形TIMEFRAME_RECTANGLE"、标签文本"TIMEFRAME_TEXT"及指标单元格(如"RSI_RECTANGLE"、"STOCH_RECTANGLE"等),其位置根据"panel_x"和 "panel_y"动态计算,保留原有布局但可以重新定位。我们调用ChartRedraw函数更新显示效果。与上一版本固定坐标(如632, 40)不同,本函数使用动态坐标,实现仪表盘组件可拖拽,并新增最小化/最大化切换按钮。最终效果与以下示例相似:

最大化状态

接下来,我们可以构建一个函数来生成最小化面板。

//+------------------------------------------------------------------+
//| Create minimized dashboard UI                                    |
//+------------------------------------------------------------------+
void create_minimized_dashboard() {
   create_rectangle(HEADER_PANEL, panel_x, panel_y, 617, 27, C'60,60,60', BORDER_FLAT); //--- Create header panel background
   create_label(HEADER_PANEL_ICON, CharToString(91), panel_x - 12, panel_y + 14, 18, clrAqua, "Wingdings"); //--- Create header icon
   create_label(HEADER_PANEL_TEXT, "TimeframeScanner", panel_x - 105, panel_y + 12, 13, COLOR_WHITE); //--- Create header title
   create_label(CLOSE_BUTTON, CharToString('r'), panel_x - 600, panel_y + 14, 18, clrYellow, "Webdings"); //--- Create close button
   create_label(TOGGLE_BUTTON, CharToString('o'), panel_x - 570, panel_y + 14, 18, clrYellow, "Wingdings"); //--- Create maximize button (+)
   ChartRedraw(0);
}

为支持切换至紧凑视图,我们定义"create_minimized_dashboard"函数。我们使用"create_rectangle"在"panel_x"和"panel_y"处创建标题面板"HEADER_PANEL",通过"create_label"添加"HEADER_PANEL_ICON"和"HEADER_PANEL_TEXT",包含"CLOSE_BUTTON",并使用最大化符号(Webdings字体中的 'o')在"panel_x - 570"和"panel_y + 14"处添加切换按钮"TOGGLE_BUTTON"。我们调用"ChartRedraw"更新显示,实现可移动的最小化仪表盘状态。正如您所见,我们为最大化和最小化按钮选择Wingdings字体以保持风格一致。您可以根据喜好选择任意字体。在我们的示例中,图标字符分别为'r'(最小化)和'o'(最大化)以下是它们的集中展示效果:

WINGDINGS字体字符

当运行最小化面板状态时,结果如下:

最小化状态

借助这两个函数,我们就能根据用户指令切换仪表盘视图模式(完整视图/紧凑视图)。由于涉及多个图形对象,我们需要集中管理对象的销毁逻辑——每次切换视图时,必须先删除旧对象,再创建新对象。

//+------------------------------------------------------------------+
//| Delete all dashboard objects                                     |
//+------------------------------------------------------------------+
void delete_all_objects() {
   ObjectDelete(0, MAIN_PANEL);                                  //--- Delete main panel
   ObjectDelete(0, HEADER_PANEL);                                //--- Delete header panel
   ObjectDelete(0, HEADER_PANEL_ICON);                           //--- Delete header icon
   ObjectDelete(0, HEADER_PANEL_TEXT);                           //--- Delete header title
   ObjectDelete(0, CLOSE_BUTTON);                                //--- Delete close button
   ObjectDelete(0, TOGGLE_BUTTON);                               //--- Delete toggle button

   ObjectsDeleteAll(0, SYMBOL_RECTANGLE);                        //--- Delete all symbol rectangles
   ObjectsDeleteAll(0, SYMBOL_TEXT);                             //--- Delete all symbol labels
   ObjectsDeleteAll(0, TIMEFRAME_RECTANGLE);                     //--- Delete all timeframe rectangles
   ObjectsDeleteAll(0, TIMEFRAME_TEXT);                          //--- Delete all timeframe labels
   ObjectsDeleteAll(0, HEADER_RECTANGLE);                        //--- Delete all header rectangles
   ObjectsDeleteAll(0, HEADER_TEXT);                             //--- Delete all header labels
   ObjectsDeleteAll(0, RSI_RECTANGLE);                           //--- Delete all RSI rectangles
   ObjectsDeleteAll(0, RSI_TEXT);                                //--- Delete all RSI labels
   ObjectsDeleteAll(0, STOCH_RECTANGLE);                         //--- Delete all Stochastic rectangles
   ObjectsDeleteAll(0, STOCH_TEXT);                              //--- Delete all Stochastic labels
   ObjectsDeleteAll(0, CCI_RECTANGLE);                           //--- Delete all CCI rectangles
   ObjectsDeleteAll(0, CCI_TEXT);                                //--- Delete all CCI labels
   ObjectsDeleteAll(0, ADX_RECTANGLE);                           //--- Delete all ADX rectangles
   ObjectsDeleteAll(0, ADX_TEXT);                                //--- Delete all ADX labels
   ObjectsDeleteAll(0, AO_RECTANGLE);                            //--- Delete all AO rectangles
   ObjectsDeleteAll(0, AO_TEXT);                                 //--- Delete all AO labels
   ObjectsDeleteAll(0, BUY_RECTANGLE);                           //--- Delete all buy rectangles
   ObjectsDeleteAll(0, BUY_TEXT);                                //--- Delete all buy labels
   ObjectsDeleteAll(0, SELL_RECTANGLE);                          //--- Delete all sell rectangles
   ObjectsDeleteAll(0, SELL_TEXT);                               //--- Delete all sell labels
}

为了更精准地控制对象的删除时机,我们创建并更新"delete_all_objects"函数,新增对"TOGGLE_BUTTON"的移除逻辑,以支持切换功能的改进。我们将ObjectDelete函数用于"TOGGLE_BUTTON"添加到待删除对象列表中,确保切换按钮在仪表盘关闭或切换时被正确删除。我们保留对其它对象的删除操作,包括"MAIN_PANEL"、"HEADER_PANEL"、"HEADER_PANEL_ICON"、"HEADER_PANEL_TEXT"、"CLOSE_BUTTON",以及所有交易品种、时间周期、标题、指标以及信号相关的矩形和标签,通过调用ObjectsDeleteAll函数,完成对所有关联元素的清理。这一变更确保可移动、可最小化的仪表盘在隐藏或重新初始化时,能够清理所有组件(包括新增的切换按钮),保持图表整洁。

为了优化动态定位功能,我们需要构建一个函数,能够根据光标位置创建和更新仪表盘元素。以下是实现这一逻辑的方法:

//+------------------------------------------------------------------+
//| Update panel object positions                                    |
//+------------------------------------------------------------------+
void update_panel_positions() {
   // Update header and buttons
   ObjectSetInteger(0, HEADER_PANEL, OBJPROP_XDISTANCE, panel_x); //--- Set header panel x position
   ObjectSetInteger(0, HEADER_PANEL, OBJPROP_YDISTANCE, panel_y); //--- Set header panel y position
   ObjectSetInteger(0, HEADER_PANEL_ICON, OBJPROP_XDISTANCE, panel_x - 12); //--- Set header icon x position
   ObjectSetInteger(0, HEADER_PANEL_ICON, OBJPROP_YDISTANCE, panel_y + 14); //--- Set header icon y position
   ObjectSetInteger(0, HEADER_PANEL_TEXT, OBJPROP_XDISTANCE, panel_x - 105); //--- Set header title x position
   ObjectSetInteger(0, HEADER_PANEL_TEXT, OBJPROP_YDISTANCE, panel_y + 12); //--- Set header title y position
   ObjectSetInteger(0, CLOSE_BUTTON, OBJPROP_XDISTANCE, panel_x - 600); //--- Set close button x position
   ObjectSetInteger(0, CLOSE_BUTTON, OBJPROP_YDISTANCE, panel_y + 14); //--- Set close button y position
   ObjectSetInteger(0, TOGGLE_BUTTON, OBJPROP_XDISTANCE, panel_x - 570); //--- Set toggle button x position
   ObjectSetInteger(0, TOGGLE_BUTTON, OBJPROP_YDISTANCE, panel_y + 14); //--- Set toggle button y position

   if (!panel_minimized) {
      // Update main panel
      ObjectSetInteger(0, MAIN_PANEL, OBJPROP_XDISTANCE, panel_x); //--- Set main panel x position
      ObjectSetInteger(0, MAIN_PANEL, OBJPROP_YDISTANCE, panel_y); //--- Set main panel y position

      // Update symbol rectangle and label
      ObjectSetInteger(0, SYMBOL_RECTANGLE, OBJPROP_XDISTANCE, panel_x - 2); //--- Set symbol rectangle x position
      ObjectSetInteger(0, SYMBOL_RECTANGLE, OBJPROP_YDISTANCE, panel_y + 35); //--- Set symbol rectangle y position
      ObjectSetInteger(0, SYMBOL_TEXT, OBJPROP_XDISTANCE, panel_x - 47); //--- Set symbol text x position
      ObjectSetInteger(0, SYMBOL_TEXT, OBJPROP_YDISTANCE, panel_y + 45); //--- Set symbol text y position

      // Update header rectangles and labels
      string header_names[] = {"BUY", "SELL", "RSI", "STOCH", "CCI", "ADX", "AO"};
      for(int header_index = 0; header_index < ArraySize(header_names); header_index++) { //--- Loop through headers
         int x_offset = panel_x - WIDTH_TIMEFRAME - (header_index < 2 ? header_index * WIDTH_SIGNAL : 2 * WIDTH_SIGNAL + (header_index - 2) * WIDTH_INDICATOR) + (1 + header_index); //--- Calculate x position
         ObjectSetInteger(0, HEADER_RECTANGLE + IntegerToString(header_index), OBJPROP_XDISTANCE, x_offset); //--- Set header rectangle x position
         ObjectSetInteger(0, HEADER_RECTANGLE + IntegerToString(header_index), OBJPROP_YDISTANCE, panel_y + 35); //--- Set header rectangle y position
         ObjectSetInteger(0, HEADER_TEXT + IntegerToString(header_index), OBJPROP_XDISTANCE, x_offset - (header_index < 2 ? WIDTH_SIGNAL/2 : WIDTH_INDICATOR/2)); //--- Set header text x position
         ObjectSetInteger(0, HEADER_TEXT + IntegerToString(header_index), OBJPROP_YDISTANCE, panel_y + 45); //--- Set header text y position
      }

      // Update timeframe rectangles, labels, and cells
      for(int timeframe_index = 0; timeframe_index < ArraySize(timeframes_array); timeframe_index++) { //--- Loop through timeframes
         int y_offset = (panel_y + 35 + HEIGHT_RECTANGLE) + timeframe_index * HEIGHT_RECTANGLE - (1 + timeframe_index); //--- Calculate y position
         ObjectSetInteger(0, TIMEFRAME_RECTANGLE + IntegerToString(timeframe_index), OBJPROP_XDISTANCE, panel_x - 2); //--- Set timeframe rectangle x position
         ObjectSetInteger(0, TIMEFRAME_RECTANGLE + IntegerToString(timeframe_index), OBJPROP_YDISTANCE, y_offset); //--- Set timeframe rectangle y position
         ObjectSetInteger(0, TIMEFRAME_TEXT + IntegerToString(timeframe_index), OBJPROP_XDISTANCE, panel_x - 47); //--- Set timeframe text x position
         ObjectSetInteger(0, TIMEFRAME_TEXT + IntegerToString(timeframe_index), OBJPROP_YDISTANCE, y_offset + 10); //--- Set timeframe text y position

         for(int header_index = 0; header_index < ArraySize(header_names); header_index++) { //--- Loop through cells
            string cell_rectangle_name, cell_text_name;
            switch(header_index) { //--- Select cell type
               case 0: cell_rectangle_name = BUY_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = BUY_TEXT + IntegerToString(timeframe_index); break; //--- Buy cell
               case 1: cell_rectangle_name = SELL_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = SELL_TEXT + IntegerToString(timeframe_index); break; //--- Sell cell
               case 2: cell_rectangle_name = RSI_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = RSI_TEXT + IntegerToString(timeframe_index); break; //--- RSI cell
               case 3: cell_rectangle_name = STOCH_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = STOCH_TEXT + IntegerToString(timeframe_index); break; //--- Stochastic cell
               case 4: cell_rectangle_name = CCI_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = CCI_TEXT + IntegerToString(timeframe_index); break; //--- CCI cell
               case 5: cell_rectangle_name = ADX_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = ADX_TEXT + IntegerToString(timeframe_index); break; //--- ADX cell
               case 6: cell_rectangle_name = AO_RECTANGLE + IntegerToString(timeframe_index); cell_text_name = AO_TEXT + IntegerToString(timeframe_index); break; //--- AO cell
            }
            int x_offset = panel_x - WIDTH_TIMEFRAME - (header_index < 2 ? header_index * WIDTH_SIGNAL : 2 * WIDTH_SIGNAL + (header_index - 2) * WIDTH_INDICATOR) + (1 + header_index); //--- Calculate x position
            int width = (header_index < 2 ? WIDTH_SIGNAL : WIDTH_INDICATOR); //--- Set cell width
            ObjectSetInteger(0, cell_rectangle_name, OBJPROP_XDISTANCE, x_offset); //--- Set cell rectangle x position
            ObjectSetInteger(0, cell_rectangle_name, OBJPROP_YDISTANCE, y_offset); //--- Set cell rectangle y position
            ObjectSetInteger(0, cell_text_name, OBJPROP_XDISTANCE, x_offset - width/2); //--- Set cell text x position
            ObjectSetInteger(0, cell_text_name, OBJPROP_YDISTANCE, y_offset + 10); //--- Set cell text y position
         }
      }
   }
   ChartRedraw(0);                                               //--- Redraw chart
}

为支持仪表盘的动态定位,我们引入新的"update_panel_positions"函数。该函数根据当前的"panel_x"和"panel_y"坐标调整所有仪表盘元素的位置,以便能够在图表上拖拽仪表盘。我们使用ObjectSetInteger函数配合OBJPROP_XDISTANCE和"OBJPROP_YDISTANCE"更新标题面板、图标、标题文本、关闭按钮和切换按钮("HEADER_PANEL"、"HEADER_PANEL_ICON"、"HEADER_PANEL_TEXT"、"CLOSE_BUTTON"、"TOGGLE_BUTTON")的位置,使它们全部基于"panel_x"和"panel_y"定位。

如果"panel_minimized"为 false,我们重新定位主面板"MAIN_PANEL"、品种矩形"SYMBOL_RECTANGLE"与标签"SYMBOL_TEXT"、标题矩形"HEADER_RECTANGLE"与标签"HEADER_TEXT",以及时间周期矩形"TIMEFRAME_RECTANGLE"、标签文本"TIMEFRAME_TEXT"和指标单元格("BUY_RECTANGLE"、"RSI_RECTANGLE"等),并使用基于"panel_x"和"panel_y"计算的偏移量。我们调用ChartRedraw函数刷新显示效果。该函数确保拖拽仪表盘时所有元素同步移动,这是可移动仪表盘优化功能的关键特性。接下来,我们来测试仪表盘。为此,我们将在OnInit事件处理器中调用创建完整仪表盘的函数,并在OnDeinit事件处理器中调用销毁函数。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()                                                     //--- Initialize EA
{
   create_full_dashboard();                                      //--- Create full dashboard
   ArraySetAsSeries(rsi_values, true);                           //--- Set RSI array as timeseries
   ArraySetAsSeries(stochastic_values, true);                    //--- Set Stochastic array as timeseries
   ArraySetAsSeries(cci_values, true);                           //--- Set CCI array as timeseries
   ArraySetAsSeries(adx_values, true);                           //--- Set ADX array as timeseries
   ArraySetAsSeries(ao_values, true);                            //--- Set AO array as timeseries
    
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);             //--- Enable mouse move events
   return(INIT_SUCCEEDED);                                       //--- Return initialization success
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)                                  //--- Deinitialize EA
{
   delete_all_objects();                                         //--- Delete all objects
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false);            //--- Disable mouse move events
   ChartRedraw(0);                                               //--- Redraw chart
}

在此阶段,我们仅在程序初始化和反初始化时调用相应函数,以便在进入下一阶段之前测试基本响应。更规范的编程方式是始终分阶段编译程序,以确保其正常运行。编译后,我们得到以下结果:

初始化和反初始化GIF图

从可视化效果来看,我们已成功实现面板的初始化与移除功能。接下来,我们将开始实现仪表盘的交互响应性。我们需要追踪坐标位置,以判断其是否位于标题栏或者按钮区域,从而明确用户的具体操作意图。我们还需获取按钮的移动路径,以便通过改变颜色来感知和可视化悬停状态与点击状态。首先,让我们定义一个函数来确定光标相对于仪表盘元素的位置。

//+------------------------------------------------------------------+
//| Check if cursor is inside header or buttons                      |
//+------------------------------------------------------------------+
bool is_cursor_in_header_or_buttons(int mouse_x, int mouse_y) {
   int chart_width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   
   // Header panel bounds
   int header_x = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_XDISTANCE);
   int header_y = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_YDISTANCE);
   int header_width = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_XSIZE);
   int header_height = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_YSIZE);
   int header_left = chart_width - header_x;
   int header_right = header_left + header_width;
   bool in_header = (mouse_x >= header_left && mouse_x <= header_right && 
                     mouse_y >= header_y && mouse_y <= header_y + header_height);

   // Close button bounds
   int close_x = (int)ObjectGetInteger(0, CLOSE_BUTTON, OBJPROP_XDISTANCE);
   int close_y = (int)ObjectGetInteger(0, CLOSE_BUTTON, OBJPROP_YDISTANCE);
   int close_width = 20;
   int close_height = 20;
   int close_left = chart_width - close_x;
   int close_right = close_left + close_width;
   bool in_close = (mouse_x >= close_left && mouse_x <= close_right && 
                    mouse_y >= close_y && mouse_y <= close_y + close_height);

   // Toggle button bounds
   int toggle_x = (int)ObjectGetInteger(0, TOGGLE_BUTTON, OBJPROP_XDISTANCE);
   int toggle_y = (int)ObjectGetInteger(0, TOGGLE_BUTTON, OBJPROP_YDISTANCE);
   int toggle_width = 20;
   int toggle_height = 20;
   int toggle_left = chart_width - toggle_x;
   int toggle_right = toggle_left + toggle_width;
   bool in_toggle = (mouse_x >= toggle_left && mouse_x <= toggle_right && 
                     mouse_y >= toggle_y && mouse_y <= toggle_y + toggle_height);

   return in_header || in_close || in_toggle;
}

首先,我们引入一个全新的"is_cursor_in_header_or_buttons"函数,用于支持仪表盘的动态定位和切换功能。该函数会检测光标是否悬停在标题面板、关闭按钮或切换按钮上,从而启用交互式拖拽和按钮操作。我们首先使用ChartGetInteger配合CHART_WIDTH_IN_PIXELS获取图表宽度。对于标题面板,我们使用ObjectGetInteger配合"OBJPROP_XDISTANCE"、"OBJPROP_YDISTANCE"、"OBJPROP_XSIZE"和OBJPROP_YSIZE,获取"HEADER_PANEL"的坐标和尺寸属性,如"header_x"、"header_y"、"header_width"和 "header_height",并且相对于图表宽度计算"header_left"和"header_right"。我们检查"mouse_x"和"mouse_y"是否在此范围内,如果是,则将"in_header"设置为true。

对于关闭按钮,我们获取"CLOSE_BUTTON"的"close_x"和"close_y",定义一个20×20像素的点击区域,计算"close_left"和"close_right",如果光标在此区域内,则将"in_close"设置为true。类似地,对于切换按钮,我们获取"TOGGLE_BUTTON"的"toggle_x"和"toggle_y",定义一个20×20像素的点击区域,如果光标在此区域内,则将"in_toggle"设置为true。如果光标在任一区域内("in_header", "in_close"或"in_toggle"),我们返回true。该函数对于检测鼠标与仪表盘可拖动标题及按钮的交互至关重要,是实现优化可移动和交互功能的核心。接下来,我们可以通过颜色可视化来更新悬停状态,以便更容易识别。

//+------------------------------------------------------------------+
//| Update button hover states                                       |
//+------------------------------------------------------------------+
void update_button_hover_states(int mouse_x, int mouse_y) {
   int chart_width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   
   // Close button hover
   int close_x = (int)ObjectGetInteger(0, CLOSE_BUTTON, OBJPROP_XDISTANCE);
   int close_y = (int)ObjectGetInteger(0, CLOSE_BUTTON, OBJPROP_YDISTANCE);
   int close_width = 20;
   int close_height = 20;
   int close_left = chart_width - close_x;
   int close_right = close_left + close_width;
   bool is_close_hovered = (mouse_x >= close_left && mouse_x <= close_right && 
                            mouse_y >= close_y && mouse_y <= close_y + close_height);

   if (is_close_hovered != prev_close_hovered) {
      ObjectSetInteger(0, CLOSE_BUTTON, OBJPROP_COLOR, is_close_hovered ? clrWhite : clrYellow);
      ObjectSetInteger(0, CLOSE_BUTTON, OBJPROP_BGCOLOR, is_close_hovered ? clrDodgerBlue : clrNONE);
      prev_close_hovered = is_close_hovered;
      ChartRedraw(0);
   }

   // Toggle button hover
   int toggle_x = (int)ObjectGetInteger(0, TOGGLE_BUTTON, OBJPROP_XDISTANCE);
   int toggle_y = (int)ObjectGetInteger(0, TOGGLE_BUTTON, OBJPROP_YDISTANCE);
   int toggle_width = 20;
   int toggle_height = 20;
   int toggle_left = chart_width - toggle_x;
   int toggle_right = toggle_left + toggle_width;
   bool is_toggle_hovered = (mouse_x >= toggle_left && mouse_x <= toggle_right && 
                             mouse_y >= toggle_y && mouse_y <= toggle_y + toggle_height);

   if (is_toggle_hovered != prev_toggle_hovered) {
      ObjectSetInteger(0, TOGGLE_BUTTON, OBJPROP_COLOR, is_toggle_hovered ? clrWhite : clrYellow);
      ObjectSetInteger(0, TOGGLE_BUTTON, OBJPROP_BGCOLOR, is_toggle_hovered ? clrDodgerBlue : clrNONE);
      prev_toggle_hovered = is_toggle_hovered;
      ChartRedraw(0);
   }
}

//+------------------------------------------------------------------+
//| Update header hover state                                        |
//+------------------------------------------------------------------+
void update_header_hover_state(int mouse_x, int mouse_y) {
   int chart_width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   int header_x = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_XDISTANCE);
   int header_y = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_YDISTANCE);
   int header_width = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_XSIZE);
   int header_height = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_YSIZE);
   int header_left = chart_width - header_x;
   int header_right = header_left + header_width;

   // Exclude button areas from header hover
   int close_x = (int)ObjectGetInteger(0, CLOSE_BUTTON, OBJPROP_XDISTANCE);
   int close_y = (int)ObjectGetInteger(0, CLOSE_BUTTON, OBJPROP_YDISTANCE);
   int close_width = 20;
   int close_height = 20;
   int close_left = chart_width - close_x;
   int close_right = close_left + close_width;

   int toggle_x = (int)ObjectGetInteger(0, TOGGLE_BUTTON, OBJPROP_XDISTANCE);
   int toggle_y = (int)ObjectGetInteger(0, TOGGLE_BUTTON, OBJPROP_YDISTANCE);
   int toggle_width = 20;
   int toggle_height = 20;
   int toggle_left = chart_width - toggle_x;
   int toggle_right = toggle_left + toggle_width;

   bool is_header_hovered = (mouse_x >= header_left && mouse_x <= header_right && 
                             mouse_y >= header_y && mouse_y <= header_y + header_height &&
                             !(mouse_x >= close_left && mouse_x <= close_right && 
                               mouse_y >= close_y && mouse_y <= close_y + close_height) &&
                             !(mouse_x >= toggle_left && mouse_x <= toggle_right && 
                               mouse_y >= toggle_y && mouse_y <= toggle_y + toggle_height));

   if (is_header_hovered != prev_header_hovered && !panel_dragging) {
      ObjectSetInteger(0, HEADER_PANEL, OBJPROP_BGCOLOR, is_header_hovered ? clrRed : C'60,60,60');
      prev_header_hovered = is_header_hovered;
      ChartRedraw(0);
   }

   update_button_hover_states(mouse_x, mouse_y);
}

在此阶段,我们引入两个新函数"update_button_hover_states"和"update_header_hover_state",为用户交互添加视觉反馈,提升了仪表盘的易用性。我们从"update_button_hover_states"函数开始,该函数接收"mouse_x"和"mouse_y"以检测关闭按钮和切换按钮上的悬停。对于"CLOSE_BUTTON",我们使用ObjectGetInteger配合"OBJPROP_XDISTANCE"和"OBJPROP_YDISTANCE"获取"close_x"和"close_y",根据ChartGetInteger配合"CHART_WIDTH_IN_PIXELS"获取的图表宽度计算20×20像素区域,如果光标在此区域内,则设置"is_close_hovered"。

如果"is_close_hovered"与"prev_close_hovered"不同,我们使用"ObjectSetInteger"更新"CLOSE_BUTTON":悬停时设置"OBJPROP_COLOR"为"clrWhite"、OBJPROP_BGCOLOR为"clrDodgerBlue";未悬停时设置 OBJPROP_COLOR 为 clrYellow、OBJPROP_BGCOLOR 为 clrNONE。之后更新"prev_close_hovered"并调用ChartRedraw函数。类似地,对于"TOGGLE_BUTTON",我们获取"toggle_x"和"toggle_y",检查20×20像素区域,如果"is_toggle_hovered"发生变更,则更新其颜色和"prev_toggle_hovered",确保按钮反馈相应足够灵敏。

接下来,我们构建"update_header_hover_state"函数,同样接收"mouse_x"和"mouse_y"。我们通过"ObjectGetInteger"获取"HEADER_PANEL"的 "header_x"、"header_y"、"header_width"和 "header_height",计算标题栏边界,并排除"CLOSE_BUTTON"和"TOGGLE_BUTTON"的区域(各20×20像素),以避免重叠。如果"is_header_hovered"与"prev_header_hovered"不同且"panel_dragging"为 false,我们更新"HEADER_PANEL"的"OBJPROP_BGCOLOR":悬停时为"clrRed",未悬停时为"C'60,60,60'",然后更新"prev_header_hovered"并调用 "ChartRedraw"。接下来调用"update_button_hover_states"确保按钮状态同步更新。这些函数为拖拽和按钮交互提供视觉提示,优化仪表盘的交互性。然后我们可以在OnChartEvent事件处理器中使用这些函数以实现完整的功能。我们应用的逻辑如下:

//+------------------------------------------------------------------+
//| Expert chart event handler                                       |
//+------------------------------------------------------------------+
void OnChartEvent(const int event_id, const long& long_param, const double& double_param, const string& string_param)
{
   if (event_id == CHARTEVENT_OBJECT_CLICK) {                    //--- Handle object click event
      if (string_param == CLOSE_BUTTON) {                        //--- Check if close button clicked
         Print("Closing the panel now");                         //--- Log panel closure
         PlaySound("alert.wav");                                 //--- Play alert sound
         panel_is_visible = false;                               //--- Hide panel
         delete_all_objects();                                   //--- Delete all objects
         ChartRedraw(0);                                         //--- Redraw chart
      } else if (string_param == TOGGLE_BUTTON) {                //--- Toggle button clicked
         delete_all_objects();                                   //--- Delete current UI
         panel_minimized = !panel_minimized;                     //--- Toggle minimized state
         if (panel_minimized) {
            Print("Minimizing the panel");                       //--- Log minimization
            create_minimized_dashboard();                        //--- Create minimized UI
         } else {
            Print("Maximizing the panel");                       //--- Log maximization
            create_full_dashboard();                             //--- Create full UI
         }
         // Reset hover states after toggle
         prev_header_hovered = false;
         prev_close_hovered = false;
         prev_toggle_hovered = false;
         ObjectSetInteger(0, HEADER_PANEL, OBJPROP_BGCOLOR, C'60,60,60');
         ObjectSetInteger(0, CLOSE_BUTTON, OBJPROP_COLOR, clrYellow);
         ObjectSetInteger(0, CLOSE_BUTTON, OBJPROP_BGCOLOR, clrNONE);
         ObjectSetInteger(0, TOGGLE_BUTTON, OBJPROP_COLOR, clrYellow);
         ObjectSetInteger(0, TOGGLE_BUTTON, OBJPROP_BGCOLOR, clrNONE);
         ChartRedraw(0);
      }
   }
   else if (event_id == CHARTEVENT_MOUSE_MOVE && panel_is_visible) { //--- Handle mouse move events
      int mouse_x = (int)long_param;                             //--- Get mouse x-coordinate
      int mouse_y = (int)double_param;                           //--- Get mouse y-coordinate
      int mouse_state = (int)string_param;                       //--- Get mouse state

      if (mouse_x == last_mouse_x && mouse_y == last_mouse_y && !panel_dragging) { //--- Skip redundant updates
         return;
      }
      last_mouse_x = mouse_x;                                    //--- Update last mouse x position
      last_mouse_y = mouse_y;                                    //--- Update last mouse y position

      update_header_hover_state(mouse_x, mouse_y);               //--- Update header and button hover states

      int chart_width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
      int header_x = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_XDISTANCE);
      int header_y = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_YDISTANCE);
      int header_width = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_XSIZE);
      int header_height = (int)ObjectGetInteger(0, HEADER_PANEL, OBJPROP_YSIZE);
      int header_left = chart_width - header_x;
      int header_right = header_left + header_width;

      int close_x = (int)ObjectGetInteger(0, CLOSE_BUTTON, OBJPROP_XDISTANCE);
      int close_width = 20;
      int close_left = chart_width - close_x;
      int close_right = close_left + close_width;

      int toggle_x = (int)ObjectGetInteger(0, TOGGLE_BUTTON, OBJPROP_XDISTANCE);
      int toggle_width = 20;
      int toggle_left = chart_width - toggle_x;
      int toggle_right = toggle_left + toggle_width;

      if (prev_mouse_state == 0 && mouse_state == 1) {           //--- Detect mouse button down
         if (mouse_x >= header_left && mouse_x <= header_right && 
             mouse_y >= header_y && mouse_y <= header_y + header_height &&
             !(mouse_x >= close_left && mouse_x <= close_right) &&
             !(mouse_x >= toggle_left && mouse_x <= toggle_right)) { //--- Exclude button areas
            panel_dragging = true;                              //--- Start dragging
            panel_drag_x = mouse_x;                             //--- Store mouse x-coordinate
            panel_drag_y = mouse_y;                             //--- Store mouse y-coordinate
            panel_start_x = header_x;                           //--- Store panel x-coordinate
            panel_start_y = header_y;                           //--- Store panel y-coordinate
            ObjectSetInteger(0, HEADER_PANEL, OBJPROP_BGCOLOR, clrMediumBlue); //--- Set header to blue on drag start
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false);      //--- Disable chart scrolling
         }
      }

      if (panel_dragging && mouse_state == 1) {                  //--- Handle dragging
         int dx = mouse_x - panel_drag_x;                        //--- Calculate x displacement
         int dy = mouse_y - panel_drag_y;                        //--- Calculate y displacement
         panel_x = panel_start_x - dx;                           //--- Update panel x-position (inverted for CORNER_RIGHT_UPPER)
         panel_y = panel_start_y + dy;                           //--- Update panel y-position

         int chart_width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get chart width
         int chart_height = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Get chart height
         panel_x = MathMax(617, MathMin(chart_width, panel_x));  //--- Keep panel within right edge
         panel_y = MathMax(0, MathMin(chart_height - (panel_minimized ? 27 : 374), panel_y)); //--- Adjust height based on state

         update_panel_positions();                               //--- Update all panel object positions
         ChartRedraw(0);                                         //--- Redraw chart during dragging
      }

      if (mouse_state == 0 && prev_mouse_state == 1) {           //--- Detect mouse button release
         if (panel_dragging) {
            panel_dragging = false;                              //--- Stop dragging
            update_header_hover_state(mouse_x, mouse_y);          //--- Update hover state immediately after drag
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true);        //--- Re-enable chart scrolling
            ChartRedraw(0);                                      //--- Redraw chart
         }
      }

      prev_mouse_state = mouse_state;                            //--- Update previous mouse state
   }
}

在此阶段,我们通过更新OnChartEvent事件处理器来优化程序,支持动态定位和切换功能,较之前版本有显著提升。我们保留对"CLOSE_BUTTON"的CHARTEVENT_OBJECT_CLICK处理:使用Print记录关闭日志,使用PlaySound播放音效,将"panel_is_visible"设置为false,调用"delete_all_objects"函数,并重绘图表。

新增功能是处理"TOGGLE_BUTTON"点击:我们调用"delete_all_objects"函数,切换"panel_minimized"状态,再根据状态选择创建最小化仪表盘"create_minimized_dashboard"(记录"最小化面板")或完整仪表盘 "create_full_dashboard"(记录"最大化面板")。我们将悬停状态("prev_header_hovered"、"prev_close_hovered"、"prev_toggle_hovered")重置为false,使用ObjectSetInteger函数恢复 "HEADER_PANEL"、"CLOSE_BUTTON"和"TOGGLE_BUTTON"的默认颜色,并重绘图表。

对于动态定位,当"panel_is_visible"为true时,我们添加CHARTEVENT_MOUSE_MOVE处理。从事件参数获取"mouse_x"、"mouse_y"和"mouse_state",如果坐标与"last_mouse_x"和"last_mouse_y"相同,且"panel_dragging"为false,则跳过冗余的更新,否则,更新这些记录的坐标。我们调用"update_header_hover_state"管理悬停效果。如果"prev_mouse_state"为0且"mouse_state"为 1,我们使用"ObjectGetInteger"和ChartGetInteger检查光标是否在"HEADER_PANEL"上(排除"CLOSE_BUTTON"和"TOGGLE_BUTTON"区域),然后设置"panel_dragging" 为true,将坐标存入"panel_drag_x"、"panel_drag_y"、"panel_start_x"和"panel_start_y",设置"HEADER_PANEL"颜色为"clrMediumBlue",并通过"ChartSetInteger"禁用图表滚动。

当"panel_dragging"和"mouse_state"均为1时,我们计算位移,在图表边界内更新"panel_x"和"panel_y",调用"update_panel_positions",并重绘图表。当鼠标释放时,我们停止拖拽,更新悬停状态,重新启用滚动,并重绘。之后更新"prev_mouse_state"。这些改动实现了拖拽和切换功能,与之前仅支持静态点击的版本不同。编译后,我们得到以下结果:

拖拽、悬停和可视化状态

从可视化效果来看,仪表盘已经初具雏形,但我们需要处理事件处理器之间的冲突。我们需要赋予某个事件处理器优先权,以提高效率。例如,当我们处于悬停、拖动状态,甚至最小化模式时,需要优先处理图表事件。如果处于休眠状态或最大化状态,则需要优先更新仪表盘。我们将通过修改行情数据(tick)更新事件来实现这一目标,因为这是更新仪表盘数据的核心环节。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()                                                     //--- Handle tick events
{
   if (panel_is_visible && !panel_minimized && !is_cursor_in_header_or_buttons(last_mouse_x, last_mouse_y)) { //--- Update indicators only if panel is visible, not minimized, and cursor is not in header/buttons
      updateIndicators();                                        //--- Update indicators
   }
}

我们通过改进OnTick事件处理器,优化了指标的更新逻辑。与之前仅根据"panel_is_visible"状态调用"updateIndicators"函数的版本不同,我们现在新增了条件:仅当"panel_is_visible"为true,且"panel_minimized"为 false,且光标未悬停在标题栏或按钮区域(通过"is_cursor_in_header_or_buttons"函数,结合"last_mouse_x"和"last_mouse_y"坐标判断)此修改将确保当仪表盘最小化,或用户正在与标题栏、关闭按钮、切换按钮交互时,暂停指标更新,从而减少拖拽或切换操作期间的冗余计算,提升整体性能。编译后,呈现如下效果:

性能提升GIF图

由可视化结果可见,仪表盘的性能得到了显著提升,且所有预设目标均已达成。当前仅需完成项目可操作性的测试工作,该部分内容已在前文章节中详细阐述。


回测

我们已完成测试,以下是整合后的可视化结果,以单张图形交换格式(GIF)位图图像形式呈现。

仪表盘回测


结论

总体而言,我们在第三部分可移动界面的基础上,为MQL5多时间周期扫描仪表盘新增了动态定位与切换功能,实现了窗口最小化/最大化切换及交互式悬停效果,显著提升了用户的操控体验。我们通过"create_minimized_dashboard"和"update_header_hover_state"等函数演示了这些改进的具体实现方式,确保新功能与现有指标网格无缝集成,为实时交易分析提供流畅体验。用户可根据自身交易需求进一步自定义该仪表盘,从而更高效地监控多周期市场信号,提升决策效率。 

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

最近评论 | 前往讨论 (2)
linfo2
linfo2 | 17 7月 2025 在 17:38
谢谢 Allan,这很酷,记录详实,涵盖了我不知道的功能,非常感谢。
Allan Munene Mutiiria
Allan Munene Mutiiria | 17 7月 2025 在 18:58
linfo2 #:
谢谢 Allan,这很酷,记录得很好,而且涵盖了我不知道的功能,非常感谢。
@linfo2 非常欢迎。谢谢。
从基础到中级:事件(二) 从基础到中级:事件(二)
在本文中,我们将看到并非所有内容都需要以某种特定的方式实现。解决问题还有其他方法。要正确理解这篇文章,有必要掌握前几篇文章中描述的概念。此处提供的材料仅用于教育目的。不要将其视为已完成的应用程序,它的目的是研究这里提出的概念。
MQL5交易策略自动化(第二十二部分):构建基于包络线趋势交易的区间补仓系统 MQL5交易策略自动化(第二十二部分):构建基于包络线趋势交易的区间补仓系统
本文中,我们在MQL5中开发了一个与包络线(Envelopes)趋势交易策略集成的区间补仓系统。我们概述了利用相对强弱指标(RSI)和包络线指标触发交易,并通过管理补仓区域来减轻亏损的架构。通过实现和回测,我们展示了如何为动态市场构建一套有效的自动化交易系统。
您应当知道的 MQL5 向导技术(第 64 部分):运用 DeMarker 和包络通道形态,搭配白噪内核 您应当知道的 MQL5 向导技术(第 64 部分):运用 DeMarker 和包络通道形态,搭配白噪内核
DeMarker 振荡器和包络指标是动量和支撑/阻力工具,能够在开发智能系统时配对。我们延续上一篇文章,概述在机器学习中加入把这对指标。我们正在使用一个循环神经网络,利用白噪内核来处理来自这两个指标的向量化信号。这是在一个自定义信号类文件中完成,其与 MQL5 向导汇编的智能系统搭配工作。
从新手到专家:使用 MQL5 制作动画新闻标题(七)—— 新闻交易的后冲击策略 从新手到专家:使用 MQL5 制作动画新闻标题(七)—— 新闻交易的后冲击策略
在重大经济新闻发布后的第一分钟内,市场出现剧烈波动的风险极高。在那短暂的时间窗口内,价格走势可能不稳定且波动剧烈,经常会触发两个方向的挂单。在发布后不久 —— 通常在一分钟内 —— 市场趋于稳定,恢复或纠正更典型的波动性。在本节中,我们将探讨新闻交易的另一种方法,旨在评估其作为交易者工具包中有价值的补充的有效性。继续阅读,了解本讨论中的更多见解和细节。