English Русский Español Deutsch 日本語 Português
preview
在 MQL5 中创建交互式图形用户界面(第 2 部分):添加控制和响应

在 MQL5 中创建交互式图形用户界面(第 2 部分):添加控制和响应

MetaTrader 5交易 | 19 二月 2025, 09:08
630 0
Allan Munene Mutiiria
Allan Munene Mutiiria

概述

上一篇文章中,我们通过编写 MetaQuotes Language 5(MQL5) 图形用户界面 (GUI) 面板的图形元素奠定了基础。如果你还记得,迭代是图形用户界面元素的静态组合 - 仅仅是定格在时间中的快照,缺乏响应性。它是静止的、没有反应的。现在,让我们解冻快照,为其注入活力。在这部万众期待的续集中,我们将把面板讨论推向新的高度。准备好,让我们一起探索如何为界面注入活力:

  • 布局和响应性:忘掉静态组件吧!我们将采用相对定位方法、灵活的布局以及可点击、可响应和可编辑的组件,使我们的面板能够响应用户的交互。
  • 动态更新:实时数据是每个交易应用程序的核心。我们将深入获取实时价格信息,并确保我们的面板反映最新的市场信息。
  • 组件移动性:想象一下可拖动元素 - 可对用户的触碰做出反应的面板。我们将探索如何使某些组件可移动,从而增强用户体验。

以下主题将指导我们如何建立一个反应灵敏、互动性强的面板:

  1. 待自动化元素示意图
  2. MQL5 中的图形用户界面自动化
  3. 结论


待自动化元素示意图

有七个组件将实现自动化。第一个组件是单击关闭按钮时面板的关闭。我们打算在单击此按钮时删除所有面板元素。其次,当点击仓位管理按钮时,按钮将根据指示关闭各自的仓位和订单。例如,当我们点击 "盈利" 按钮或标签时,就会关闭所有盈利的仓位。第三个自动化将针对交易量组件。单击它的实体后,将创建一个选项的下拉列表,供用户选择交易选项。

第四项自动化是在相应交易按钮旁边的增加或减少按钮上,它们可以增加或减少编辑栏中的值,而不是直接键入。如果用户想直接输入所需的值,编辑栏就需要捕捉输入的值,这就是我们的第五个自动化步骤。然后,第六步将是在悬停按钮上创建悬停效果。也就是说,当鼠标位于悬停按钮区域内时,按钮会变大,表明鼠标位于按钮附近;当鼠标离开按钮区域时,按钮会重置为默认状态。最后,我们会在每个价格分时上将报价更新为实时值。 

为了便于理解这些自动化流程和组件,下文将详细介绍这些流程和组件在上一个里程碑中的特点。

步骤的示意图

了解我们将要做的事情后,让我们立即开始自动化。如果您还没有阅读上一篇文章,请参阅我们创建图形用户界面元素静态组件的文章,这样您就能与我们一起步入正轨。让我们开始吧。


MQL5 中的图形用户界面自动化

我们将从简单流程到复杂流程,这样我们的结构就能按时间顺序排列。因此,我们会在每个分时或报价变化时更新价格。为此,我们需要 OnTick 事件处理程序,这是一个内置的 MQL5 函数,通常在报价发生变化时调用。函数是 void 数据类型,这意味着它直接处理执行,无需返回任何输出。您的函数应如下所示。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){

  ...

}
//+------------------------------------------------------------------+

这是负责价格更新的事件处理程序,因此也是我们逻辑的核心。我们将在该函数中添加控制逻辑,如下所示:

    // --- Update price quotes ---
    
    // Set the text of the "SELL PRICE" label to the current Bid price
    ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, Bid());

我们使用 ObjectSetString 设置对象的属性(本例中为文本),因为我们需要更改按钮标签的文本输入。我们为当前图表提供的窗口图表 ID 为 0,或者您也可以提供 "ChartID()" 函数,该函数将提供当前图表窗口的图表标识索引。然后,我们提供 "LABEL_SELL_PRICE" 作为目标对象名称,以更新卖出按钮标签,并提供 "OBJPROP_TEXT",表示我们要更新的对象属性是对象的文本字符串值。最后,我们提供文本内容。买家报价(bid)是我们需要更新的值,因此我们要填写它,但这还不是全部。我们需要填写的属性类型是字符串型数据,而我们的买家报价是双精度型格式。因此,我们需要将双精度型的值转换为字符串类型的值,否则会在编译时收到警告 - 从 "数字" 到 "字符串" 的隐式转换。

警告描述

此时,我们可以直接将双精度型数值转换为字符串,如下图所示,但通常不建议这样做,因为使用时应深思熟虑。

    // --- Update price quotes ---
    
    // Set the text of the "SELL PRICE" label to the current Bid price
    ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, (string)Bid());

MQL5 中的类型转换与将数值转换为字符串一样,有一些细微差别,其中最常见的是精度损失。例如,我们的买家报价数值是一个浮点数值,当其价格为 11.77900 时,最后两个零将被忽略,最终输出值为 11.779。从技术上讲,这两个数值在逻辑上没有区别,但从视觉上看,一个包含 5 位数,另一个包含 3 位数,这在数学上是有区别的。下面举例说明我们的意思。

类型z转换的细微差别

正如我们所看到的,类型转换可以消除警告,但在精确度很重要的情况下,这并不是最好的方法。因此,需要另一个函数。我们使用 MQL5 内置的 DoubleToString 函数进行转换。此函数用于将带浮点数的数值转换为文本字符串。它需要两个输入参数或自变量,目标浮点数值和精度格式。在本例中,我们使用买家报价作为目标值,精度格式为 _Digits,这是一个存储小数点后位数的变量,用于定义当前图表中交易品种的价格精度。您也可以使用 Digits() 函数。这将是 0 至 8 范围内的任意数字,如果省略,它将假定为 8 位数。例如,我们的交易品种是黄金 (XAUUSD),有三位小数。因此,我们将 3 作为位数值,但为了自动化并使代码适应货币对,我们使用该函数自动检索特定货币对的位数。但是,如果您想要一个固定的小数位数范围,请使用固定值。以下是设置买家报价价格的最终代码。 

    // --- Update price quotes ---
    
    // Set the text of the "SELL PRICE" label to the current Bid price
    ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, DoubleToString(Bid(), _Digits));

现在我们有了正确的转换逻辑,感谢 MQL5 开发人员提供的漂亮函数,我们将得到下面的结果。

正确的买家报价价格表示

设置买入按钮的卖家报价和点差的逻辑也是如此。下面是相关代码。 

    // --- Update price quotes ---
    
    // Set the text of the "SELL PRICE" label to the current Bid price
    ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, DoubleToString(Bid(), _Digits));
    
    // Set the text of the "BUY PRICE" label to the current Ask price
    ObjectSetString(0, LABEL_BUY_PRICE, OBJPROP_TEXT, DoubleToString(Ask(), _Digits));
    
    // Set the text of the "SPREAD" button to the current spread value
    ObjectSetString(0, BTN_SPREAD, OBJPROP_TEXT, (string)Spread());

你应该已经注意到,对于点差,我们直接类型转换了它的字符串值,尽管我们以前曾批评过这种保持准确性的方法。在这里,点差是一种整数数据类型,因此准确性并不是最重要的,无论如何,我们都将获得正确的格式。不过,您也可以使用 IntegerToString 函数进行转换,这样也会得到相同的值。

    // Set the text of the "SPREAD" button to the current spread value
    ObjectSetString(0, BTN_SPREAD, OBJPROP_TEXT, IntegerToString(Spread()));

该函数需要三个参数,但只有目标值就足够了,因为它没有指定精度格式。现在,您可以了解其中的差别了。在下面的 GIF 图片中,就是我们目前取得的成果。

价格 GIF

这就是我们需要在事件处理函数上做的全部工作,负责更新价格的完整源代码如下:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){
    // --- Update price quotes ---
    
    // Set the text of the "SELL PRICE" label to the current Bid price
    ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, DoubleToString(Bid(), _Digits));
    
    // Set the text of the "BUY PRICE" label to the current Ask price
    ObjectSetString(0, LABEL_BUY_PRICE, OBJPROP_TEXT, DoubleToString(Ask(), _Digits));
    
    // Set the text of the "SPREAD" button to the current spread value
    ObjectSetString(0, BTN_SPREAD, OBJPROP_TEXT, IntegerToString(Spread()));
}
//+------------------------------------------------------------------+

现在,第一个自动化组件已经完成,很简单的,对吧?然后,我们开始处理图形用户界面面板的其他组件。其余元素的自动化将在 OnChartEvent 函数处理程序中完成,让我们深入了解一下它的输入参数及其功能。

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{

        ...

}

该函数的目的是处理用户或 MQL5 程序所做的图表更改。因此,用户移动鼠标、编辑按钮字段、点击标签和按钮等交互操作都将由该事件处理函数捕获和处理。让我们对其参数进行细分,以获得更多解读:

  • id:此参数表示事件 ID,对应于 11 种预定义事件类型之一。这些包括按键、鼠标移动、对象创建、图表更改和自定义事件等事件。对于自定义事件,您可以使用从 CHARTEVENT_CUSTOM 到 CHARTEVENT_CUSTOM_LAST 之间的 ID。11 种事件类型如下所示;

图表事件类型

  • lparam:长整型事件参数。其值取决于所处理的具体事件。例如,它可以表示按键事件中的按键代码。
  • dparam:双精度型事件参数。与 lparam 类似,其值根据事件类型而变化。例如,在鼠标移动事件中,它可能会表示鼠标光标的位置。
  • sparam:字符串类型的事件参数。其含义同样取决于事件类型。例如,在对象创建过程中,它可以保存新创建对象的名称。

为了更容易理解地展示这一点,在函数中,让我们打印一份包含日志所有四个参数的打印输出。

// Print the 4 function parameters    
Print("ID = ",id,", LPARAM = ",lparam,", DPARAM = ",dparam,", SPARAM = ",sparam);

此函数将打印图表事件 ID、长整型事件值、双精度型事件值和字符串类型值。让我们看看下面的 GIF,以便于参考。

图表事件 GIF

从提供的 GIF 中,一切都应该清楚了。现在我们开始捕捉 GUI 面板元素上的图表点击事件。因此,我们的 ID 将是 "CHARTEVENT_OBJECT_CLICK"。 

   //Print("ID = ",id,", LPARAM = ",lparam,", DPARAM = ",dparam,", SPARAM = ",sparam);

   if (id==CHARTEVENT_OBJECT_CLICK){
        
        ...

   }

我们首先注释掉前一行代码,因为我们不想在日志中添加无关的信息。使用的两条斜线 (//) 称为单行注释,从一行的起始开始注释代码,直到该行结束,因此称为 "单行" 注释。在执行过程中,计算机会特意忽略注释。我们使用 if 语句来检查是否有对象点击。这是通过判断图表事件 ID 与对象点击枚举相等来实现的。如果我们确实点击了一个对象,让我们打印参数,看看会得到什么。使用的代码如下。 

   if (id==CHARTEVENT_OBJECT_CLICK){
      Print("ID = ",id,", LM = ",lparam,", DM = ",dparam,", SPARAM = ",sparam);

      ...

   }

在打印输出函数中,我们只是将 "LPARAM" 改为 "LP",将 "DPARAM" 改为 "DP",这样我们就可以只关注图表事件 ID 和被点击对象的名称,从中获取对象的 ID 并在必要时采取行动。下面是逻辑的图解:

对象点击 GIF

点击救护车图标后,第一个组件自动化功能将在 GUI 面板销毁时发生。从上面的 GIF 中可以看到,一旦某个对象被点击,该对象的名称就会存储在字符串-事件类型变量中。因此,从这个变量中,我们可以得到被点击对象的名称,并检查它是否是我们想要的对象,如果是这样,我们可以采取行动,在我们的例子中销毁面板。 

      //--- if icon car is clicked, destroy the panel
      if (sparam==ICON_CAR){
         Print("BTN CAR CLICKED. DESTROY PANEL NOW");
         destroyPanel();
         ChartRedraw(0);
      }

另一个 if 语句用于检查汽车图标被点击的实例,如果是这种情况,我们就会通知该实例它被点击了,然后就可以销毁面板了,因为它是负责此项工作的图标。之后,我们调用 "destroyPanel" 函数,其目的是删除面板中的每个元素。这个函数大家应该已经很熟悉了,因为我们在上一篇文章(即第一部分)中使用过它。最后,我们调用 ChartRedraw 函数。该函数用于强制重绘指定图表。以编程方式修改图表属性或对象(如指标、线条或形状)时,更改可能不会立即反映在图表上。通过调用它,可以确保图表更新并显示最新的变化。下面是我们得到的直观结果。

面板销毁 GIF

由此可见,逻辑非常简单,其它对象的点击也采用同样的方法。现在让我们继续处理点击关闭按钮标签时发生的事件。发生这种情况时,我们需要关闭所有未平仓位,删除所有挂单。这将确保我们没有任何市场订单。需要一个 else-if 语句来检查是否点击了关闭按钮的条件。

      else if (sparam == BTN_CLOSE) {
          // Button "Close" clicked. Close all orders and positions now.
          Print("BTN CLOSE CLICKED. CLOSE ALL ORDERS & POSITIONS NOW");
      
          // Store the original color of the button
          long originalColor = ObjectGetInteger(0, BTN_CLOSE, OBJPROP_COLOR);
      
          // Change the button color to red (for visual feedback)
          ObjectSetInteger(0, BTN_CLOSE, OBJPROP_COLOR, clrRed);

          ...

      }

在这里,我们要对事件实例稍作调整。我们希望点击按钮后,按钮的颜色会发生变化,表明按钮已被点击,从而启动关闭市场订单的过程。关闭完成后,我们需要将按钮颜色重置为默认颜色属性。为了获取按钮标签的原始颜色,我们声明了一个名为 "originalColor" 的长整型数据变量,并在其中存储了按钮的默认颜色。要获取按钮的颜色,我们需要使用 ObjectGetInteger 函数,并输入图表 ID、按钮名称和按钮属性(本例中为颜色)。在存储了原始颜色后,我们就可以修改按钮标签的颜色了,因为我们已经保留了它的原始值。我们使用 ObjectSetInteger 将对象的颜色设置为红色。在这种状态下,我们会启动订单关闭过程。

          // Iterate through all open positions
          for (int i = 0; i <= PositionsTotal(); i++) {
              ulong ticket = PositionGetTicket(i);
              if (ticket > 0) {
                  if (PositionSelectByTicket(ticket)) {
                      // Check if the position symbol matches the current chart symbol
                      if (PositionGetString(POSITION_SYMBOL) == _Symbol) {
                          obj_Trade.PositionClose(ticket); // Close the position
                      }
                  }
              }
          }

我们使用 for 循环遍历所有未平仓位并将其平仓。要获得所有未平仓位,我们使用 MQL5 内置函数 PositionsTotal。该函数将返回指定交易账户的未平仓位数量。然后,我们通过在 PositionGetTicket 函数中提供仓位索引来获取该仓位的编号,并将其存储在名为 "ticket" 的 ulong 数据类型变量中。该函数返回指定仓位的编号,如果失败,则返回 0。我们必须确保有编号才能继续。这是通过使用 if 语句来确保编号值大于 0 来实现的。如果是这样,就意味着我们有一个编号,我们继续选择编号,这样就可以使用它了。如果我们成功选择了编号,就可以检索到该仓位的信息。由于该特定交易账户中可能有几个仓位,因此我们确保只关闭与该特定货币对相关的仓位。最后,我们根据仓位编号关闭该仓位,并对其他未平仓仓位(如果有的话)进行同样的操作。

但是,在平仓时,我们使用 "obj_Trade",后面跟一个点运算符。这就是所谓的类对象。为了方便地进行平仓操作,我们需要加入一个类实例来辅助这一过程。因此,我们可以在源代码开头使用 #include 来包含一个交易实例。这样,我们就可以访问 CTrade 类,用它来创建一个交易对象。这一点至关重要,因为我们需要它来进行交易操作。

#include <Trade/Trade.mqh>
CTrade obj_Trade;

预处理器将用 Trade.mqh 文件的内容替换 #include <Trade/Trade.mqh> 代码行。角括号表示 Trade.mqh 文件将从标准目录(通常是 terminal_installation_directory\MQL5\Include)中获取。搜索不包括当前目录。该行可以放在程序中的任何位置,但通常情况下,所有包含内容都放在源代码的开头,这样代码结构更合理,也更容易参考。通过声明 CTrade 类的 obj_Trade 对象,我们可以轻松访问该类中包含的方法,这要归功于 MQL5 开发人员。

CTrade 类

要删除挂单,也要使用相同的迭代逻辑。

          // Iterate through all pending orders
          for (int i = 0; i <= OrdersTotal(); i++) {
              ulong ticket = OrderGetTicket(i);
              if (ticket > 0) {
                  if (OrderSelect(ticket)) {
                      // Check if the order symbol matches the current chart symbol
                      if (OrderGetString(ORDER_SYMBOL) == _Symbol) {
                          obj_Trade.OrderDelete(ticket); // Delete the order
                      }
                  }
              }
          }

迭代逻辑的主要区别在于,我们使用 OrdersTotal 函数来获取订单总数。其它一切都与订单有关。在关闭所有仓位并删除订单后,我们需要将按钮标签颜色重置为原来的颜色。 

          // Reset the button color to its original value
          Print("Resetting button to original color");
          ObjectSetInteger(0, BTN_CLOSE, OBJPROP_COLOR, originalColor);
      
          // Force a redrawing of the chart to reflect the changes
          ChartRedraw(0);

使用 "ObjectSetInteger" 函数时,需要输入图表 ID、按钮名称、颜色属性和原始颜色。这时,我们事先准备的变量就派上用场了。我们不必总是记住对象的原始颜色,而可以自动存储和检索。负责关闭所有未平仓位和删除所有未结订单的完整代码如下:

      else if (sparam == BTN_CLOSE) {
          // Button "Close" clicked. Close all orders and positions now.
          Print("BTN CLOSE CLICKED. CLOSE ALL ORDERS & POSITIONS NOW");
      
          // Store the original color of the button
          long originalColor = ObjectGetInteger(0, BTN_CLOSE, OBJPROP_COLOR);
      
          // Change the button color to red (for visual feedback)
          ObjectSetInteger(0, BTN_CLOSE, OBJPROP_COLOR, clrRed);
      
          // Iterate through all open positions
          for (int i = 0; i <= PositionsTotal(); i++) {
              ulong ticket = PositionGetTicket(i);
              if (ticket > 0) {
                  if (PositionSelectByTicket(ticket)) {
                      // Check if the position symbol matches the current chart symbol
                      if (PositionGetString(POSITION_SYMBOL) == _Symbol) {
                          obj_Trade.PositionClose(ticket); // Close the position
                      }
                  }
              }
          }
      
          // Iterate through all pending orders
          for (int i = 0; i <= OrdersTotal(); i++) {
              ulong ticket = OrderGetTicket(i);
              if (ticket > 0) {
                  if (OrderSelect(ticket)) {
                      // Check if the order symbol matches the current chart symbol
                      if (OrderGetString(ORDER_SYMBOL) == _Symbol) {
                          obj_Trade.OrderDelete(ticket); // Delete the order
                      }
                  }
              }
          }
      
          // Reset the button color to its original value
          Print("Resetting button to original color");
          ObjectSetInteger(0, BTN_CLOSE, OBJPROP_COLOR, originalColor);
      
          // Force a redrawing of the chart to reflect the changes
          ChartRedraw(0);
      }

建议每次在面板上添加逻辑后,都要编译并运行代码,以确保在升级到另一个控制逻辑之前,一切都能按预期运行。这就是我们目前取得的成就。

关闭 GIF

现在,我们可以成功关闭所有仓位和订单。请注意,当点击关闭按钮时,仓位正在关闭,按钮的标签颜色保持红色,直到所有仓位都关闭,最后恢复原来的颜色。同样,您可以注意到,我们没有关闭 "AUDUSD" 买入仓位,因为 EA 交易系统当前关联的是黄金交易品种。现在,可以用同样的逻辑来设置其他按钮标签的响应。 

      else if (sparam == BTN_MARKET) {
          // Button "Market" clicked. Close all positions related to the current chart symbol.
          Print(sparam + " CLICKED. CLOSE ALL POSITIONS NOW");
      
          // Iterate through all open positions
          for (int i = 0; i <= PositionsTotal(); i++) {
              ulong ticket = PositionGetTicket(i);
              if (ticket > 0) {
                  if (PositionSelectByTicket(ticket)) {
                      // Check if the position symbol matches the current chart symbol
                      if (PositionGetString(POSITION_SYMBOL) == _Symbol) {
                          obj_Trade.PositionClose(ticket); // Close the position
                      }
                  }
              }
          }
      
          // Force a redrawing of the chart to reflect the changes
          ChartRedraw(0);
      }

这段代码与平仓按钮代码的不同之处在于,我们取消了订单关闭迭代,因为我们只想关闭所有已打开的仓位。要关闭所有盈利仓位,可使用下面的代码片段。

      else if (sparam == BTN_PROFIT) {
          // Button "Profit" clicked. Close all positions in profit now.
          Print(sparam + " CLICKED. CLOSE ALL POSITIONS IN PROFIT NOW");
      
          // Iterate through all open positions
          for (int i = 0; i <= PositionsTotal(); i++) {
              ulong ticket = PositionGetTicket(i);
              if (ticket > 0) {
                  if (PositionSelectByTicket(ticket)) {
                      // Check if the position symbol matches the current chart symbol
                      if (PositionGetString(POSITION_SYMBOL) == _Symbol) {
                          double profit_or_loss = PositionGetDouble(POSITION_PROFIT);
                          if (profit_or_loss > 0) {
                              obj_Trade.PositionClose(ticket); // Close the position
                          }
                      }
                  }
              }
          }
      
          // Force a redrawing of the chart to reflect the changes
          ChartRedraw(0);
      }

这段代码与前一段代码的主要区别在于,前一段代码是关闭所有已建立的仓位,而这段代码则增加了一个额外的逻辑来检查仓位的利润是否大于零,也就是说,我们只关闭有利润的头寸。具体逻辑如下:

                          double profit_or_loss = PositionGetDouble(POSITION_PROFIT);
                          if (profit_or_loss > 0) {
                              obj_Trade.PositionClose(ticket); // Close the position
                          }

我们定义一个名为 "profit_or_loss" 的双精度数据类型变量,并在其中存储所选仓位的当前浮动盈亏。如果数值大于 0,我们就平仓,因为已经获利。下图中的亏损按钮也采用了同样的逻辑,只有在亏损的情况下,我们才会平仓。 

      else if (sparam == BTN_LOSS) {
          // Button "Loss" clicked. Close all positions in loss now.
          Print(sparam + " CLICKED. CLOSE ALL POSITIONS IN LOSS NOW");
      
          // Iterate through all open positions
          for (int i = 0; i <= PositionsTotal(); i++) {
              ulong ticket = PositionGetTicket(i);
              if (ticket > 0) {
                  if (PositionSelectByTicket(ticket)) {
                      // Check if the position symbol matches the current chart symbol
                      if (PositionGetString(POSITION_SYMBOL) == _Symbol) {
                          double profit_or_loss = PositionGetDouble(POSITION_PROFIT);
                          if (profit_or_loss < 0) {
                              obj_Trade.PositionClose(ticket); // Close the position
                          }
                      }
                  }
              }
          }
      
          // Force a redrawing of the chart to reflect the changes
          ChartRedraw(0);
      }

最后,当点击挂单按钮标签时,要关闭挂单,就需要使用订单迭代,其代码如下。

      else if (sparam == BTN_PENDING) {
          // Button "Pending" clicked. Delete all pending orders related to the current chart symbol.
          Print(sparam + " CLICKED. DELETE ALL PENDING ORDERS NOW");
      
          // Iterate through all pending orders
          for (int i = 0; i <= OrdersTotal(); i++) {
              ulong ticket = OrderGetTicket(i);
              if (ticket > 0) {
                  if (OrderSelect(ticket)) {
                      // Check if the order symbol matches the current chart symbol
                      if (OrderGetString(ORDER_SYMBOL) == _Symbol) {
                          obj_Trade.OrderDelete(ticket); // Delete the order
                      }
                  }
              }
          }
      
          // Force a redrawing of the chart to reflect the changes
          ChartRedraw(0);
      }

以下是本里程碑的可视化展示。

所有关闭按钮 GIF

如图所示,很明显,我们的面板标题按钮现在点击时会有反应。现在,我们开始为交易量按钮增添活力。我们希望,当单击按钮或标签本身或单击下拉图标时,我们会创建另一个子面板,其中包含用户可以选择的各种选项列表。逻辑如下:

      else if (sparam == BTN_LOTS || sparam == LABEL_LOTS || sparam == ICON_DROP_DN1) {
          // Button "Lots," label "Lots," or dropdown icon clicked. Create a dropdown list.
          Print(sparam + " CLICKED. CREATE A DROPDOWN LIST");
      
          // Enable the button for dropdown functionality
          ObjectSetInteger(0, BTN_LOTS, OBJPROP_STATE, true);
      
          // Create the dropdown list
          createDropDown();
      
          // Redraw the chart to reflect the changes
          ChartRedraw(0);
      }

点击按钮后,我们会通知实例,并将按钮的状态设置为 true。这将使按钮变暗,表明按钮已被点击。这样,我们就可以调用自定义的 "createDropDown" 函数来创建下拉列表,该函数的代码片段已在第一篇文章中提供。创建完成后,用户必须从选项中进行选择。因此,如果通过点击选择了一个选项,我们就必须捕捉它并将按钮的标签设置为用户的选择,并销毁选项面板的下拉列表。我们使用下面的代码片段来实现这一目的。

      else if (sparam == LABEL_OPT1) {
          // Label "Lots" clicked.
          Print("LABEL LOTS CLICKED");
      
          // Get the text from LABEL_OPT1
          string text = ObjectGetString(0, LABEL_OPT1, OBJPROP_TEXT);
      
          // Get the state of the button (enabled or disabled)
          bool btnState = ObjectGetInteger(0, BTN_LOTS, OBJPROP_STATE);
      
          // Set the text of LABEL_LOTS to match LABEL_OPT1
          ObjectSetString(0, LABEL_LOTS, OBJPROP_TEXT, text);
      
          // Destroy the dropdown list
          destroyDropDown();
      
          // If the button was previously enabled, disable it
          if (btnState == true) {
              ObjectSetInteger(0, BTN_LOTS, OBJPROP_STATE, false);
          }
      
          // Redraw the chart
          ChartRedraw(0);
      }

首先,我们检查第一个选项是否被点击。如果是,我们会获取所选选项的文本值,并将其设置为交易量按钮的文本值。我们使用一个自定义的 "destroyDropDown" 函数,在将用户的选择设置为按钮状态后,删除已创建的子面板,其代码片段如下。

//+------------------------------------------------------------------+
//|    Function to destroy dropdown                                  |
//+------------------------------------------------------------------+

void destroyDropDown(){
   ObjectDelete(0,BTN_DROP_DN);
   ObjectDelete(0,LABEL_OPT1);
   ObjectDelete(0,LABEL_OPT2);
   ObjectDelete(0,LABEL_OPT3);
   ObjectDelete(0,ICON_DRAG);
   ChartRedraw(0);
}

最后,我们会检查按钮的状态是否已启用,即是否处于点击模式,如果是,我们会将状态属性设置为 false,从而禁用按钮。选项中也采用了同样的逻辑。其代码片段如下:

      else if (sparam==LABEL_OPT2){
         Print("LABEL RISK % CLICKED");
         string text = ObjectGetString(0,LABEL_OPT2,OBJPROP_TEXT);
         bool btnState = ObjectGetInteger(0,BTN_LOTS,OBJPROP_STATE);
         ObjectSetString(0,LABEL_LOTS,OBJPROP_TEXT,text);
         destroyDropDown();
         if (btnState==true){
            ObjectSetInteger(0,BTN_LOTS,OBJPROP_STATE,false);
         }
         ChartRedraw(0);
      }
      else if (sparam==LABEL_OPT3){
         Print("LABEL MONEY CLICKED");
         string text = ObjectGetString(0,LABEL_OPT3,OBJPROP_TEXT);
         bool btnState = ObjectGetInteger(0,BTN_LOTS,OBJPROP_STATE);
         ObjectSetString(0,LABEL_LOTS,OBJPROP_TEXT,text);
         destroyDropDown();
         if (btnState==true){
            ObjectSetInteger(0,BTN_LOTS,OBJPROP_STATE,false);
         }
         ChartRedraw(0);
      }

当点击侧边按钮,即增加和减少按钮时,我们需要通过增加或减少相应编辑字段的值来使其响应。首先,让我们来看看交易量增加按钮。

      else if (sparam == BTN_P1) {
          // Button "P1" clicked. Increase trading volume.
          Print(sparam + " CLICKED. INCREASE TRADING VOLUME");
      
          // Get the current trading volume from EDIT_LOTS
          double trade_lots = (double)ObjectGetString(0, EDIT_LOTS, OBJPROP_TEXT);
      
          // Increment the trading volume by 0.01
          trade_lots += 0.01;
      
          // Update the value in EDIT_LOTS
          ObjectSetString(0, EDIT_LOTS, OBJPROP_TEXT, DoubleToString(trade_lots, 2));
      
          // Redraw the chart
          ChartRedraw(0);
      }

如果点击了交易量增加按钮,我们就会通知该实例,并准备通过获取其当前值来增加手数字段的值。在获取到的交易量基础上,我们再增加 0.01 作为增加的步长值。操作符 "+=" 的使用简化了这一过程。其典型作用是将手数大小的值增加 0.01。这等同于说(trade_lots = trade_lots + 0.01)。然后将结果传递给手数的字段。双精度型数值转换为字符串,精度为 2 位数。同样的逻辑也适用于减少按钮,只是我们需要从数值中减去 0.01。 

      else if (sparam == BTN_M1) {
          // Button "M1" clicked. Decrease trading volume.
          Print(sparam + " CLICKED. DECREASE TRADING VOLUME");
      
          // Get the current trading volume from EDIT_LOTS
          double trade_lots = (double)ObjectGetString(0, EDIT_LOTS, OBJPROP_TEXT);
      
          // Decrease the trading volume by 0.01
          trade_lots -= 0.01;
      
          // Update the value in EDIT_LOTS
          ObjectSetString(0, EDIT_LOTS, OBJPROP_TEXT, DoubleToString(trade_lots, 2));
      
          // Redraw the chart
          ChartRedraw(0);
      }

同样的道理也适用于其他类似的按钮。

      else if (sparam==BTN_P2){
         Print(sparam+" CLICKED. INCREASE STOP LOSS POINTS");
         double sl_points = (double)ObjectGetString(0,EDIT_SL,OBJPROP_TEXT);
         sl_points+=10.0;
         ObjectSetString(0,EDIT_SL,OBJPROP_TEXT,DoubleToString(sl_points,1));
         ChartRedraw(0);
      }
      else if (sparam==BTN_M2){
         Print(sparam+" CLICKED. DECREASE STOP LOSS POINTS");
         double sl_points = (double)ObjectGetString(0,EDIT_SL,OBJPROP_TEXT);
         sl_points-=10.0;
         ObjectSetString(0,EDIT_SL,OBJPROP_TEXT,DoubleToString(sl_points,1));
         ChartRedraw(0);
      }
      
      else if (sparam==BTN_P3){
         Print(sparam+" CLICKED. INCREASE STOP LOSS POINTS");
         double tp_points = (double)ObjectGetString(0,EDIT_TP,OBJPROP_TEXT);
         tp_points+=10.0;
         ObjectSetString(0,EDIT_TP,OBJPROP_TEXT,DoubleToString(tp_points,1));
         ChartRedraw(0);
      }
      else if (sparam==BTN_M3){
         Print(sparam+" CLICKED. DECREASE STOP LOSS POINTS");
         double tp_points = (double)ObjectGetString(0,EDIT_TP,OBJPROP_TEXT);
         tp_points-=10.0;
         ObjectSetString(0,EDIT_TP,OBJPROP_TEXT,DoubleToString(tp_points,1));
         ChartRedraw(0);
      }

在这里,我们将止损和止盈值指定为 10 点。为了确定我们的方向是正确的,我们编译代码并运行得到以下可视化结果。

下拉 增加 减少 按钮

到目前为止,进展良好。其余的按钮还有卖出和买入按钮。它们的逻辑也很简单,与前面的逻辑如出一辙。对于卖出按钮,我们的逻辑如下。

      else if (sparam==BTN_SELL){
         Print("BTN SELL CLICKED");
         ObjectSetInteger(0,BTN_SELL,OBJPROP_STATE,false);
         double trade_lots = (double)ObjectGetString(0,EDIT_LOTS,OBJPROP_TEXT);
         double sell_sl = (double)ObjectGetString(0,EDIT_SL,OBJPROP_TEXT);
         sell_sl = Ask()+sell_sl*_Point;
         sell_sl = NormalizeDouble(sell_sl,_Digits);
         double sell_tp = (double)ObjectGetString(0,EDIT_TP,OBJPROP_TEXT);
         sell_tp = Ask()-sell_tp*_Point;
         sell_tp = NormalizeDouble(sell_tp,_Digits);

         Print("Lots = ",trade_lots,", SL = ",sell_sl,", TP = ",sell_tp);
         obj_Trade.Sell(trade_lots,_Symbol,Bid(),sell_sl,sell_tp);
         ChartRedraw();
      }

如果点击事件发生在卖出按钮上,我们会通知该实例,并将按钮的状态设置为 false,表示我们已启用点击选项。要建立卖出仓位,我们需要交易量、止损点数和止盈点数。我们获取这些值并将其存储在指定的变量中,以便于获取。要计算止损,我们需要将止损点数乘以 _Point,然后将所得值与当前要价相加,从而将止损点转换为兼容货币对的点数格式。随后,我们将双精度输出值归一化为交易品种的位数,以确保准确性和精确度。同样的方法也适用于止盈位,最后,我们建立一个卖出仓位,将交易手数、买家报价作为卖出价、止损和止盈。同样的逻辑也适用于买入仓位,其逻辑如下。

      else if (sparam==BTN_BUY){
         Print("BTN BUY CLICKED");
         ObjectSetInteger(0,BTN_BUY,OBJPROP_STATE,false);
         double trade_lots = (double)ObjectGetString(0,EDIT_LOTS,OBJPROP_TEXT);
         double buy_sl = (double)ObjectGetString(0,EDIT_SL,OBJPROP_TEXT);
         buy_sl = Bid()-buy_sl*_Point;
         buy_sl = NormalizeDouble(buy_sl,_Digits);
         double buy_tp = (double)ObjectGetString(0,EDIT_TP,OBJPROP_TEXT);
         buy_tp = Bid()+buy_tp*_Point;
         buy_tp = NormalizeDouble(buy_tp,_Digits);

         Print("Lots = ",trade_lots,", SL = ",buy_sl,", TP = ",buy_tp);
         obj_Trade.Buy(trade_lots,_Symbol,Ask(),buy_sl,buy_tp);
         ChartRedraw();
      }

测试结果如下:

买入卖出 GIF

到目前为止,一切都在按预期进行。用户可以选择不使用增加和减少按钮,而是直接使用编辑按钮字段中的编辑选项。在此过程中,编辑时可能会出现不可预见的错误,导致操作被忽略。例如,用户可能输入 "0.Q7" 的手数大小。从技术上讲,这个数值并不完全是一个数字,因为它确实包含字母 "Q"。因此,在此手数下不会有任何交易操作。因此,我们要确保该值始终有效,如果无效,则提示一个需要纠正错误的实例。为此,使用了另一个图表事件 ID "CHARTEVENT_OBJECT_ENDEDIT"。 

   else if (id==CHARTEVENT_OBJECT_ENDEDIT){
      if (sparam==EDIT_LOTS){
         Print(sparam+" WAS JUST EDITED. CHECK FOR ANY UNFORESEEN ERRORS");
         string user_lots = ObjectGetString(0,EDIT_LOTS,OBJPROP_TEXT);

         ...   

      }
   }

首先,我们要检查图表事件 ID 是否是编辑字段的结束编辑。如果是,我们会检查编辑字段是否为交易量按钮;如果是,我们会通知实例并获取用户输入值,以便进一步分析潜在的意外错误。输入内容存储在名为 "user_lots" 的字符串变量中。为了进行分析,我们需要将手数大小分割成若干部分,我们的边界将由句号(.)字符定义 - 通常称为句号、点或圆点。

         string lots_Parts_Array[];
         int splitCounts = StringSplit(user_lots,'.',lots_Parts_Array);//rep '.' = 'a' 
      
         Print("User lots split counts = ",splitCounts);ArrayPrint(lots_Parts_Array,0,"<&> ");

我们将拆分部分的动态存储数组定义为名为 "lots_Parts_Array" 的字符串数据类型变量。然后,我们借助需要 3 个参数的 StringSplit 函数分割用户输入。我们提供要分割的目标字符串值(本例中为用户手数大小输入),然后提供句点作为分隔符,最后提供一个存储子串的数组。该函数将返回存储数组中子串的数量。如果在传递的字符串中找不到指定的分隔符,数组中将只放入一个源字符串。这些分割计数将存储在分割计数变量中。最后,我们会打印分割计数的结果以及数组值,即生成的子串。如果我们将手数大小编辑为 0.05,得到的结果如下:

编辑手数分割

要使输入值有效,应有一个句号分隔符,从而产生两个分割计数。如果是,则表示输入具有一个句号分隔符。

         if (splitCounts == 2){

            ...

         }

如果分割计数等于 1,则表示输入缺少句号,因此无法接受。在这种情况下,我们会通知错误并将名为 "isInputValid" 的布尔变量设置为 false。

         else if (splitCounts == 1){
            Print("ERROR: YOUR INPUT MUST CONTAIN DECIMAL POINTS");
            isInputValid = false;
         }

如果两个条件都不满足,则意味着输入的分隔符超过了 1 个,这是错误的,因此我们会通知错误并将输入有效标志设置为 false。

         else {
            Print("ERROR: YOU CAN NOT HAVE MORE THAN ONE DECIMAL POINT IN INPUT");
            isInputValid = false;
         }

如果我们输入一个非无效值,并用两个句号分隔,专家日志的输出结果就是这样。

输入两个句号

要检查输入中是否存在非数字字符,我们必须在两个分割的每个字符中循环,并逐个进行评估。这需要通过一个 for 循环来轻松实现。

            if (StringLen(lots_Parts_Array[0]) > 0){
            
               //
... 

            }

首先,我们要确保存储数组中索引 0 处的第一个字符串不是空字符串,即字符串长度大于 0 的情况。StringLen 函数用于获取字符串中的符号数。如果字符串中的字符数小于或等于 0,则表示该子串为空,输入值已经无效。

            else {
               Print("ERROR: PART 1 (LEFT HAND SIDE) IS EMPTY");
               isInputValid = false;
            }

为了使错误更直观,下面是我们将分隔符左侧部分留空后得到的结果。

左侧部分空

为了检查非数字字符,我们使用了一个 for 循环,如下所示。

               string split = lots_Parts_Array[0];
               for (int i=0; i<StringLen(split); i++){
                  ushort symbol_code = StringGetCharacter(split,i);
                  string character = StringSubstr(split,i,1);
                  if (!(symbol_code >= 48 && symbol_code <= 57)){
                     Print("ERROR: @ index ",i+1," (",character,") is NOT a numeral. Code = ",symbol_code);
                     isInputValid = false;
                     break;
                  }
               }

我们定义了一个名为 "split" 的字符串变量,用于在存储数组中存储第一个子串。然后,我们遍历子串中的所有字符。对于选定的字符,我们使用StringGetCharacter 函数获取字符代码,该函数返回字符串指定位置的字符值,并将字符代码存储在名为 "symbol_code" 的无符号短整型变量中。要获取实际的符号字符,我们使用字符串 substring 函数。最后,我们使用 if 语句来检查生成的代码是否属于数字代码,如果不属于数字代码,则表示我们有一个非数字字符。因此,我们告知错误,将输入有效性标志设置为 false,并提前跳出循环。如果不是,则表示字符都是数字值,我们的输入有效性仍将为真,正如初始化时那样。

         bool isInputValid = true;

您可能已经注意到,48 至 57 之间的数字范围被视为数字符号代码范围。让我们来看看为什么。根据 ASCII 表格,这些数字字符的十进制编号系统从字符 "0" 的 48 开始,一直到字符 "9" 的 57。

交易品种代码 1

后面的内容如下。

字符代码 2

同样的逻辑也适用于拆分字符串的第二部分,即分隔符右侧的子串上。其源代码如下。

            if (StringLen(lots_Parts_Array[1]) > 0){
               string split = lots_Parts_Array[1];
               for (int i=0; i<StringLen(split); i++){
                  ushort symbol_code = StringGetCharacter(split,i);
                  string character = StringSubstr(split,i,1);
                  if (!(symbol_code >= 48 && symbol_code <= 57)){
                     Print("ERROR: @ index ",i+1," (",character,") is NOT a numeral. Code = ",symbol_code);
                     isInputValid = false;
                     break;
                  }
               }

            }
            else {
               Print("ERROR: PART 2 (RIGHT HAND SIDE) IS EMPTY");
               isInputValid = false;
            }

为了确定我们能够区分数字和非数字字符,让我们举例说明。 

非数字输入

您可以看到,当我们添加代码为 65 的大写字母 "A" 时,会返回错误信息,表明输入无效。在这个例子中,我们使用了 "A",因为它的符号代码可以很容易地在所提供的图像中找到。它可能是其它任何东西。现在,我们再次使用输入有效性标志,为相关编辑字段设置有效文本值。 

         if (isInputValid == true){
            Print("SUCCESS: INPUT IS VALID.");
            ObjectSetString(0,EDIT_LOTS,OBJPROP_TEXT,user_lots);
            ObjectSetInteger(0,EDIT_LOTS,OBJPROP_COLOR,clrBlack);
            ObjectSetInteger(0,EDIT_LOTS,OBJPROP_BGCOLOR,clrWhite);
            ChartRedraw(0);
         }

如果输入有效性标志等于 "true",我们就会通知成功,并将文本值设置为原始用户输入值,因为它不存在任何差异。我们再次将文字的颜色设置为黑色,将按钮的背景颜色设置为白色。这些通常是编辑字段的原始属性。如果输出为 false,则表示用户输入值有错误,不能用于交易操作。

         else if (isInputValid == false){
            Print("ERROR: INPUT IS INVALID. ENTER A VALID INPUT!");
            ObjectSetString(0,EDIT_LOTS,OBJPROP_TEXT,"Error");
            ObjectSetInteger(0,EDIT_LOTS,OBJPROP_COLOR,clrWhite);
            ObjectSetInteger(0,EDIT_LOTS,OBJPROP_BGCOLOR,clrRed);
            ChartRedraw(0);
         }

因此,我们会通知该错误,并将文本值设置为 "Error"(错误)。为了吸引用户的最终注意力,我们将文字颜色设置为白色,背景颜色设置为红色,这种醒目的颜色组合让用户很容易就能识别出出现了错误。编译后,我们得到了以下结果。

用户输入 GIF

至此,大部分面板组件的自动化工作已经完成。唯一没有考虑到的是下拉列表的移动和鼠标在按钮上的悬停效果。当鼠标在图表上移动时,所有这些都需要考虑,因此将考虑 "CHARTEVENT_MOUSE_MOVE" 事件 ID。为了跟踪鼠标的移动,我们需要在专家初始化实例中启用图表上的鼠标移动检测逻辑,这可以通过下面的逻辑实现。 

   //--- enable CHART_EVENT_MOUSE_MOVE detection
   ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,true);

让我们先从最简单的悬停效果开始。启用鼠标检测后,当鼠标在图表中移动时,我们就会收到该事件。 

   else if (id==CHARTEVENT_MOUSE_MOVE){

      ...

   }

要检测鼠标在图表中的位置,我们需要获得它的坐标,即分别沿 x 轴和 y 轴的位置,以及它的状态,即移动时和静止时的状态。

      int mouse_X = (int)lparam;    // mouseX   >>> mouse coordinates
      int mouse_Y = (int)dparam;    // mouseY   >>> mouse coordinates
      int mouse_State = (int)sparam; // Get the mouse state (0 = mouse moving)

在这里,我们声明了一个整数数据类型变量 "mouse_X",用于存储鼠标沿 x 轴的距离,或者说沿日期和时间刻度的距离。我们再次获取双精度型参数,并将其值存储到 "mouse_Y" 参数中,最后将字符串参数存储到 "mouse_State" 变量中。最后我们将它们类型转换为整数。我们需要目标元素的初始坐标,因此我们通过下面的代码片段来定义它们。

      //GETTING THE INITIAL DISTANCES AND SIZES OF BUTTON
      
      int XDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XDISTANCE);
      int YDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YDISTANCE);
      int XSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XSIZE);
      int YSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YSIZE);
      

我们获取各自的按钮距离和大小,并将其存储在相应的整数型变量中。类型转换格式用于将数值转换为整数格式。为了跟踪有关按钮的鼠标坐标,我们需要一些变量来保存逻辑。

      static bool prevMouseInside = false;
      bool isMouseInside = false;

声明静态 "prevMouseInside" 布尔变量是为了跟踪鼠标之前是否位于按钮区域内。"isMouseInside" 布尔变量将存储有关按钮的当前鼠标状态,所有变量都初始化为 false 标志。为了确定鼠标是否位于按钮区域内,我们使用了条件语句。

      if (mouse_X >= XDistance_Hover_Btn && mouse_X <= XDistance_Hover_Btn + XSize_Hover_Btn &&
          mouse_Y >= YDistance_Hover_Btn && mouse_Y <= YDistance_Hover_Btn + YSize_Hover_Btn){
         isMouseInside = true;
      }

条件检查可以确定鼠标光标当前是否位于按钮区域内。如果是,"isMouseInside" 将设为 true,表示鼠标位于光标内部;否则,如果不满足条件,布尔变量将设为 false。从技术上讲,必须满足四个条件,鼠标指针才能被视为位于按钮区域内。让我们分解每个条件,以便进一步理解。

  • mouse_X >= XDistance_Hover_Btn:这将检查鼠标的 X 坐标(mouse_X)是否大于或等于按钮的左边界(XDistance_Hover_Btn)。
  • mouse_X <= XDistance_Hover_Btn + XSize_Hover_Btn:这将检查鼠标的 X 坐标是否小于或等于按钮的右边界(XDistance_Hover_Btn 与按钮宽度 XSize_Hover_Btn 之和)。
  • mouse_Y >= YDistance_Hover_Btn:同样,这将检查鼠标的 Y 坐标(mouse_Y)是否大于或等于按钮的上边界(YDistance_Hover_Btn)。
  • mouse_Y <= YDistance_Hover_Btn + YSize_Hover_Btn:这将检查鼠标的 Y 坐标是否小于或等于按钮的底部边界(YDistance_Hover_Btn 与按钮高度 YSize_Hover_Btn 之和)。

如果满足所有条件,我们就会将 "isMouseInside" 变量设置为 true。利用得出的值,我们就可以检查鼠标是否在按钮内。实现了如下逻辑。 

      if (isMouseInside != prevMouseInside) {

在这里,我们要检查自上次检查以来,鼠标的当前状态(在按钮区域内还是按钮区域外)是否发生了变化。它确保只有当鼠标相对于按钮的位置发生变化时,才会执行后续操作。我们需要再次检查是否满足条件。

         // Mouse entered or left the button area
         if (isMouseInside) {
            Print("Mouse entered the Button area. Do your updates!");
            //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'050,050,255');

            ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'050,050,255');
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, clrLightBlue);
         }

如果布尔变量为 true,则表示鼠标进入了按钮区域。我们通过 print 语句通知实例。然后,我们更改按钮标签的颜色和背景。否则,如果变量为 false,则表示鼠标指针之前在按钮区域内,但刚刚离开。因此,我们将颜色重置为默认值。下面是负责该逻辑的代码片段。

         else if (!isMouseInside) {
            Print("Mouse left Btn proximities. Return default properties.");
            //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'100,100,100');
            // Reset button properties when mouse leaves the area
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'100,100,100');
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, C'220,220,220');
         }

在对按钮属性进行任何更改后,"ChartRedraw" 函数将被调用,以刷新图表显示并反映更新后的按钮外观。 最后,"prevMouseInside" 变量会被更新,以匹配鼠标的当前状态("isMouseInside")。这样就能确保下一次触发事件时,程序能将新状态与之前的状态进行比较。

         ChartRedraw(0);//// Redraw the chart to reflect the changes
         prevMouseInside = isMouseInside;

创建按钮悬停效果的完整代码如下:

   else if (id==CHARTEVENT_MOUSE_MOVE){
      int mouse_X = (int)lparam;    // mouseX   >>> mouse coordinates
      int mouse_Y = (int)dparam;    // mouseY   >>> mouse coordinates
      int mouse_State = (int)sparam; // Get the mouse state (0 = mouse moving)
      
      //GETTING THE INITIAL DISTANCES AND SIZES OF BUTTON
      
      int XDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XDISTANCE);
      int YDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YDISTANCE);
      int XSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XSIZE);
      int YSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YSIZE);
      
      static bool prevMouseInside = false;
      bool isMouseInside = false;
      
      //Print("Mouse STATE = ",mouse_State); // 0 = mouse moving

      if (mouse_X >= XDistance_Hover_Btn && mouse_X <= XDistance_Hover_Btn + XSize_Hover_Btn &&
          mouse_Y >= YDistance_Hover_Btn && mouse_Y <= YDistance_Hover_Btn + YSize_Hover_Btn){
         isMouseInside = true;
      }
      
      if (isMouseInside != prevMouseInside) {
         // Mouse entered or left the button area
         if (isMouseInside) {
            Print("Mouse entered the Button area. Do your updates!");
            //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'050,050,255');

            ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'050,050,255');
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, clrLightBlue);
         }
         else if (!isMouseInside) {
            Print("Mouse left Btn proximities. Return default properties.");
            //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'100,100,100');
            // Reset button properties when mouse leaves the area
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'100,100,100');
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, C'220,220,220');
         }
         ChartRedraw(0);//// Redraw the chart to reflect the changes
         prevMouseInside = isMouseInside;
      }
   }

经过编译,我们得到了以下结果:

悬停效果 GIF

真是太好了。现在,我们进入了最后一部分,即不仅要跟踪鼠标光标的移动,还要随之移动对象或组件。同样,我们声明了一个静态整数变量来检测鼠标何时被点击,以及一个布尔变量来存储鼠标光标的移动状态。这可以通过下面的代码片段实现。

      // CREATE MOVEMENT
      static int prevMouseClickState = false; // false = 0, true = 1;
      static bool movingState = false;

然后,我们需要初始化变量,以保存对象的大小和距离。 

      // INITIALIZE VARIBALES TO STORE INITIAL SIZES AND DISTANCES OF OBJECTS
      // MLB = MOUSE LEFT BUTTON
      static int mlbDownX = 0; // Stores the X-coordinate of the mouse left button press
      static int mlbDownY = 0; // Stores the Y-coordinate of the mouse left button press
      
      static int mlbDownX_Distance = 0; // Stores the X-distance of an object
      static int mlbDownY_Distance = 0; // Stores the Y-distance of an object
      
      static int mlbDownX_Distance_BTN_DROP_DN = 0; // Stores X-distance for a specific button (BTN_DROP_DN)
      static int mlbDownY_Distance_BTN_DROP_DN = 0; // Stores Y-distance for the same button
      
      static int mlbDownX_Distance_LABEL_OPT1 = 0; // Stores X-distance for a label (LABEL_OPT1)
      static int mlbDownY_Distance_LABEL_OPT1 = 0; // Stores Y-distance for the same label
      
      static int mlbDownX_Distance_LABEL_OPT2 = 0; // Stores X-distance for another label (LABEL_OPT2)
      static int mlbDownY_Distance_LABEL_OPT2 = 0; // Stores Y-distance for the same label
      
      static int mlbDownX_Distance_LABEL_OPT3 = 0; // Stores X-distance for yet another label (LABEL_OPT3)
      static int mlbDownY_Distance_LABEL_OPT3 = 0; // Stores Y-distance for the same label
      
      static int mlbDownX_Distance_ICON_DRAG = 0; // Stores X-distance for an icon (ICON_DRAG)
      static int mlbDownY_Distance_ICON_DRAG = 0; // Stores Y-distance for the same icon

为了初始化存储变量,我们声明静态数据型变量,并如上所述将其初始化为 0。它们被声明为静态,因为我们需要存储各自的尺寸和距离,以便在面板组件运动时作为参考。同样,我们需要初始元素的距离,这可以通过下面的代码片段实现。 

      //GET THE INITIAL DISTANCES AND SIZES OF BUTTON
      
      int XDistance_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_XDISTANCE);
      int YDistance_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_YDISTANCE);
      //int XSize_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_XSIZE);
      //int YSize_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_YSIZE);
      
      int XDistance_Opt1_Lbl = (int)ObjectGetInteger(0,LABEL_OPT1,OBJPROP_XDISTANCE);
      int YDistance_Opt1_Lbl = (int)ObjectGetInteger(0,LABEL_OPT1,OBJPROP_YDISTANCE);
      
      int XDistance_Opt2_Lbl = (int)ObjectGetInteger(0,LABEL_OPT2,OBJPROP_XDISTANCE);
      int YDistance_Opt2_Lbl = (int)ObjectGetInteger(0,LABEL_OPT2,OBJPROP_YDISTANCE);
      
      int XDistance_Opt3_Lbl = (int)ObjectGetInteger(0,LABEL_OPT3,OBJPROP_XDISTANCE);
      int YDistance_Opt3_Lbl = (int)ObjectGetInteger(0,LABEL_OPT3,OBJPROP_YDISTANCE);

      int XDistance_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_XDISTANCE);
      int YDistance_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_YDISTANCE);
      int XSize_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_XSIZE);
      int YSize_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_YSIZE);

在这里,我们只需使用 ObjectGetInteger 函数来获取要随光标移动的元素的距离。不过,请注意,我们还得到了将在面板移动中使用的图标的大小。就像在悬停效果逻辑中一样,我们之所以需要尺寸,是为了确定鼠标光标何时被点击到图标区域内,以便随后开始移动。然后,我们需要捕捉初始的鼠标点击信息,并存储要移动的对象的距离。

if (prevMouseClickState == false && mouse_State == 1) {
    // Check if the left mouse button was clicked and the mouse is in the pressed state

    // Initialize variables to store initial distances and sizes of objects
    mlbDownX = mouse_X; // Store the X-coordinate of the mouse click
    mlbDownY = mouse_Y; // Store the Y-coordinate of the mouse click

    // Store distances for specific objects
    mlbDownX_Distance = XDistance_Drag_Icon; // Distance of the drag icon (X-axis)
    mlbDownY_Distance = YDistance_Drag_Icon; // Distance of the drag icon (Y-axis)

    mlbDownX_Distance_BTN_DROP_DN = XDistance_DropDn_Btn; // Distance of a specific button (BTN_DROP_DN)
    mlbDownY_Distance_BTN_DROP_DN = YDistance_DropDn_Btn;

    mlbDownX_Distance_LABEL_OPT1 = XDistance_Opt1_Lbl; // Distance of a label (LABEL_OPT1)
    mlbDownY_Distance_LABEL_OPT1 = YDistance_Opt1_Lbl;

    mlbDownX_Distance_LABEL_OPT2 = XDistance_Opt2_Lbl; // Distance of another label (LABEL_OPT2)
    mlbDownY_Distance_LABEL_OPT2 = YDistance_Opt2_Lbl;

    mlbDownX_Distance_LABEL_OPT3 = XDistance_Opt3_Lbl; // Distance of yet another label (LABEL_OPT3)
    mlbDownY_Distance_LABEL_OPT3 = YDistance_Opt3_Lbl;

    // Check if the mouse is within the drag icon area
    if (mouse_X >= XDistance_Drag_Icon && mouse_X <= XDistance_Drag_Icon + XSize_Drag_Icon &&
        mouse_Y >= YDistance_Drag_Icon && mouse_Y <= YDistance_Drag_Icon + YSize_Drag_Icon) {
        movingState = true; // Set the moving state to true
    }
}

我们使用条件语句来检查两个条件。其一是 "prevMouseClickState == false",以确保鼠标左键之前未被点击;其二是 "mouse_State == 1",以检查鼠标当前是否处于按下状态(按纽按下)。如果满足这两个条件,我们就会存储鼠标的 X 和 Y 坐标以及对象的距离。最后,我们会检查鼠标是否位于拖动图标区域内,如果是,我们会将移动状态设置为 true,这表明我们可以开始移动面板组件。为了更便于理解,让我们把这四个条件分解一下:

  • mouse_X >= XDistance_Drag_Icon:这将验证鼠标的 X 坐标 (mouse_X) 是否大于或等于拖动图标区域的左边界 (XDistance_Drag_Icon)。
  • mouse_X <= XDistance_Drag_Icon + XSize_Drag_Icon:同样,它还会确保 X 坐标小于或等于拖动图标区域的右边界(XDistance_Drag_Icon 与图标宽度 XSize_Drag_Icon 之和)。
  • mouse_Y >= YDistance_Drag_Icon:这将检查鼠标的 Y 坐标(mouse_Y)是否大于或等于拖动图标区域的上边界(YDistance_Drag_Icon)。
  • mouse_Y <= YDistance_Drag_Icon + YSize_Drag_Icon:同样,它还会验证 Y 坐标是否小于或等于拖动图标区域的底部边界(YDistance_Drag_Icon 与图标高度 YSize_Drag_Icon 之和)。

如果满足所有四个条件(即鼠标位于定义的拖动图标区域内),我们就会将 "movingState" 变量设为 true。此时,如果移动状态为 true,我们就移动指定的对象。

      if (movingState){
         ChartSetInteger(0,CHART_MOUSE_SCROLL,false);
         
         ObjectSetInteger(0,ICON_DRAG,OBJPROP_XDISTANCE,mlbDownX_Distance + mouse_X - mlbDownX);
         ObjectSetInteger(0,ICON_DRAG,OBJPROP_YDISTANCE,mlbDownY_Distance + mouse_Y - mlbDownY);
         
         ...

         ChartRedraw(0);
      }

在这里,我们使用 ChartSetInteger 函数来禁用图表滚动标志。这将确保鼠标移动时,图表不会水平滚动。这样,只有鼠标光标会随着指定对象移动。最后,我们设置新对象的距离,涉及当前鼠标坐标,并重新绘制图表使更改生效。一言以蔽之,这就是我们现在的情况:

拖动图标 GIF

现在你可以看到,我们可以拖动图标了。不过,我们还需要将它与其他面板组件拖到一起。因此,同样的逻辑也适用。 

         ObjectSetInteger(0,BTN_DROP_DN,OBJPROP_XDISTANCE,mlbDownX_Distance_BTN_DROP_DN + mouse_X - mlbDownX);
         ObjectSetInteger(0,BTN_DROP_DN,OBJPROP_YDISTANCE,mlbDownY_Distance_BTN_DROP_DN + mouse_Y - mlbDownY);
         
         ObjectSetInteger(0,LABEL_OPT1,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT1 + mouse_X - mlbDownX);
         ObjectSetInteger(0,LABEL_OPT1,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT1 + mouse_Y - mlbDownY);
         
         ObjectSetInteger(0,LABEL_OPT2,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT2 + mouse_X - mlbDownX);
         ObjectSetInteger(0,LABEL_OPT2,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT2 + mouse_Y - mlbDownY);

         ObjectSetInteger(0,LABEL_OPT3,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT3 + mouse_X - mlbDownX);
         ObjectSetInteger(0,LABEL_OPT3,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT3 + mouse_Y - mlbDownY);

添加其它元素的拖动逻辑将确保拖动图标移动时,其他面板组件也随之移动。编译后,我们得到了这样的结果:

拖动图标粘贴 GUI

成功了,您可以看到,所有面板组件都会随着鼠标光标移动。不过,我们还需要解决一个小问题。当鼠标被释放时,即不在按压模式下,组件会随着光标的移动而继续移动。要使面板脱离移动状态,我们需要在鼠标未按下时将状态设置为 false。 

      if (mouse_State == 0){
         movingState = false;
         ChartSetInteger(0,CHART_MOUSE_SCROLL,true);
      }

如果鼠标状态等于零,则表示鼠标左键被释放,因此我们将移动状态设置为 false,表示我们不需要进一步移动面板组件。随后,我们将图表滚动标志设置为 true,从而启用图表的滚动事件。最后,我们将之前的鼠标状态设置为当前的鼠标状态。

      prevMouseClickState = mouse_State;

负责悬停效果和面板移动自动化的最终源代码如下:

   else if (id==CHARTEVENT_MOUSE_MOVE){
      int mouse_X = (int)lparam;    // mouseX   >>> mouse coordinates
      int mouse_Y = (int)dparam;    // mouseY   >>> mouse coordinates
      int mouse_State = (int)sparam; // Get the mouse state (0 = mouse moving)
      
      //GETTING THE INITIAL DISTANCES AND SIZES OF BUTTON
      
      int XDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XDISTANCE);
      int YDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YDISTANCE);
      int XSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XSIZE);
      int YSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YSIZE);
      
      static bool prevMouseInside = false;
      bool isMouseInside = false;
      
      //Print("Mouse STATE = ",mouse_State); // 0 = mouse moving

      if (mouse_X >= XDistance_Hover_Btn && mouse_X <= XDistance_Hover_Btn + XSize_Hover_Btn &&
          mouse_Y >= YDistance_Hover_Btn && mouse_Y <= YDistance_Hover_Btn + YSize_Hover_Btn){
         isMouseInside = true;
      }
      
      if (isMouseInside != prevMouseInside) {
         // Mouse entered or left the button area
         if (isMouseInside) {
            Print("Mouse entered the Button area. Do your updates!");
            //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'050,050,255');

            ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'050,050,255');
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, clrLightBlue);
         }
         else if (!isMouseInside) {
            Print("Mouse left Btn proximities. Return default properties.");
            //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'100,100,100');
            // Reset button properties when mouse leaves the area
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'100,100,100');
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, C'220,220,220');
         }
         ChartRedraw(0);//// Redraw the chart to reflect the changes
         prevMouseInside = isMouseInside;
      }
      
      // CREATE MOVEMENT
      static int prevMouseClickState = false; // false = 0, true = 1;
      static bool movingState = false;
      
      // INITIALIZE VARIBALES TO STORE INITIAL SIZES AND DISTANCES OF OBJECTS
      // MLB = MOUSE LEFT BUTTON
      static int mlbDownX = 0; // Stores the X-coordinate of the mouse left button press
      static int mlbDownY = 0; // Stores the Y-coordinate of the mouse left button press
      
      static int mlbDownX_Distance = 0; // Stores the X-distance of an object
      static int mlbDownY_Distance = 0; // Stores the Y-distance of an object
      
      static int mlbDownX_Distance_BTN_DROP_DN = 0; // Stores X-distance for a specific button (BTN_DROP_DN)
      static int mlbDownY_Distance_BTN_DROP_DN = 0; // Stores Y-distance for the same button
      
      static int mlbDownX_Distance_LABEL_OPT1 = 0; // Stores X-distance for a label (LABEL_OPT1)
      static int mlbDownY_Distance_LABEL_OPT1 = 0; // Stores Y-distance for the same label
      
      static int mlbDownX_Distance_LABEL_OPT2 = 0; // Stores X-distance for another label (LABEL_OPT2)
      static int mlbDownY_Distance_LABEL_OPT2 = 0; // Stores Y-distance for the same label
      
      static int mlbDownX_Distance_LABEL_OPT3 = 0; // Stores X-distance for yet another label (LABEL_OPT3)
      static int mlbDownY_Distance_LABEL_OPT3 = 0; // Stores Y-distance for the same label
      
      static int mlbDownX_Distance_ICON_DRAG = 0; // Stores X-distance for an icon (ICON_DRAG)
      static int mlbDownY_Distance_ICON_DRAG = 0; // Stores Y-distance for the same icon
            
            
      //GET THE INITIAL DISTANCES AND SIZES OF BUTTON
      
      int XDistance_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_XDISTANCE);
      int YDistance_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_YDISTANCE);
      //int XSize_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_XSIZE);
      //int YSize_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_YSIZE);
      
      int XDistance_Opt1_Lbl = (int)ObjectGetInteger(0,LABEL_OPT1,OBJPROP_XDISTANCE);
      int YDistance_Opt1_Lbl = (int)ObjectGetInteger(0,LABEL_OPT1,OBJPROP_YDISTANCE);
      
      int XDistance_Opt2_Lbl = (int)ObjectGetInteger(0,LABEL_OPT2,OBJPROP_XDISTANCE);
      int YDistance_Opt2_Lbl = (int)ObjectGetInteger(0,LABEL_OPT2,OBJPROP_YDISTANCE);
      
      int XDistance_Opt3_Lbl = (int)ObjectGetInteger(0,LABEL_OPT3,OBJPROP_XDISTANCE);
      int YDistance_Opt3_Lbl = (int)ObjectGetInteger(0,LABEL_OPT3,OBJPROP_YDISTANCE);

      int XDistance_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_XDISTANCE);
      int YDistance_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_YDISTANCE);
      int XSize_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_XSIZE);
      int YSize_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_YSIZE);
            
      if (prevMouseClickState == false && mouse_State == 1) {
          // Check if the left mouse button was clicked and the mouse is in the pressed state
      
          // Initialize variables to store initial distances and sizes of objects
          mlbDownX = mouse_X; // Store the X-coordinate of the mouse click
          mlbDownY = mouse_Y; // Store the Y-coordinate of the mouse click
      
          // Store distances for specific objects
          mlbDownX_Distance = XDistance_Drag_Icon; // Distance of the drag icon (X-axis)
          mlbDownY_Distance = YDistance_Drag_Icon; // Distance of the drag icon (Y-axis)
      
          mlbDownX_Distance_BTN_DROP_DN = XDistance_DropDn_Btn; // Distance of BTN_DROP_DN
          mlbDownY_Distance_BTN_DROP_DN = YDistance_DropDn_Btn;
      
          mlbDownX_Distance_LABEL_OPT1 = XDistance_Opt1_Lbl; // Distance of LABEL_OPT1
          mlbDownY_Distance_LABEL_OPT1 = YDistance_Opt1_Lbl;
      
          mlbDownX_Distance_LABEL_OPT2 = XDistance_Opt2_Lbl; // Distance of LABEL_OPT2
          mlbDownY_Distance_LABEL_OPT2 = YDistance_Opt2_Lbl;
      
          mlbDownX_Distance_LABEL_OPT3 = XDistance_Opt3_Lbl; // Distance of LABEL_OPT3
          mlbDownY_Distance_LABEL_OPT3 = YDistance_Opt3_Lbl;
      
          // Check if the mouse is within the drag icon area
          if (mouse_X >= XDistance_Drag_Icon && mouse_X <= XDistance_Drag_Icon + XSize_Drag_Icon &&
              mouse_Y >= YDistance_Drag_Icon && mouse_Y <= YDistance_Drag_Icon + YSize_Drag_Icon) {
              movingState = true; // Set the moving state to true
          }
      }
            
      if (movingState){
         ChartSetInteger(0,CHART_MOUSE_SCROLL,false);
         
         ObjectSetInteger(0,ICON_DRAG,OBJPROP_XDISTANCE,mlbDownX_Distance + mouse_X - mlbDownX);
         ObjectSetInteger(0,ICON_DRAG,OBJPROP_YDISTANCE,mlbDownY_Distance + mouse_Y - mlbDownY);
         
         ObjectSetInteger(0,BTN_DROP_DN,OBJPROP_XDISTANCE,mlbDownX_Distance_BTN_DROP_DN + mouse_X - mlbDownX);
         ObjectSetInteger(0,BTN_DROP_DN,OBJPROP_YDISTANCE,mlbDownY_Distance_BTN_DROP_DN + mouse_Y - mlbDownY);
         
         ObjectSetInteger(0,LABEL_OPT1,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT1 + mouse_X - mlbDownX);
         ObjectSetInteger(0,LABEL_OPT1,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT1 + mouse_Y - mlbDownY);
         
         ObjectSetInteger(0,LABEL_OPT2,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT2 + mouse_X - mlbDownX);
         ObjectSetInteger(0,LABEL_OPT2,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT2 + mouse_Y - mlbDownY);

         ObjectSetInteger(0,LABEL_OPT3,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT3 + mouse_X - mlbDownX);
         ObjectSetInteger(0,LABEL_OPT3,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT3 + mouse_Y - mlbDownY);

         ChartRedraw(0);
      }
      
      if (mouse_State == 0){
         movingState = false;
         ChartSetInteger(0,CHART_MOUSE_SCROLL,true);
      }
      prevMouseClickState = mouse_State;
   }

一言以蔽之,这就是我们的成果。

最终 GIF

这太棒了,我们刚刚为我们的图形用户界面面板注入了活力,现在我们的面板是可交互和响应的。它有悬停效果、按钮点击、实时数据更新,并能对鼠标移动做出反应。


结论

总之,通过本文的实现,我们可以说,将动态功能集成到 MetaQuotes Language 5(MQL5) 图形用户界面面板中,可以使其更具交互性和功能性,从而显著增强用户体验。添加按钮悬停效果可以创建一个视觉上引人入胜的界面,直观地响应用户操作。买卖价格的实时更新确保交易者拥有最新的市场信息,使他们能够快速做出明智的决策。用于执行买卖订单的可点击按钮,以及仓位和订单关闭功能,简化了交易操作,使用户能够及时对市场变化做出反应。

此外,可移动子面板和下拉列表的实现为界面增加了一层定制和灵活性。交易员可以根据自己的喜好组织工作空间,提高整体效率。下拉列表功能提供了一种方便的方式来访问各种选项,而不会使主界面变得混乱,从而有助于打造一个更清洁、更有组织的交易环境。总之,这些增强功能将 MQL5 GUI 面板转变为一个强大、用户友好的工具,满足了现代交易者的需求,最终改善了他们的交易体验和效率。交易者可以使用所示的知识来创建更复杂、更吸引人的 GUI 面板,以改善他们的交易体验。我们衷心希望您发现这篇文章详细、客观、易于理解和学习。干杯!


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

附加的文件 |
使用MQL5与Python构建自我优化的智能交易系统 使用MQL5与Python构建自我优化的智能交易系统
在本文中,我们将讨论如何构建能够根据当前市场条件自主选择和更改交易策略的EA。我们将学习马尔可夫链(Markov Chains)以及它们如何帮助我们作为算法交易者。
神经网络变得简单(第 92 部分):频域和时域中的自适应预测 神经网络变得简单(第 92 部分):频域和时域中的自适应预测
FreDF 方法的作者通过实验证实了结合频域和时域进行预测的优势。不过,权重超参数的使用对于非稳态时间序列并非最优。在本文中,我们将领略结合频域和时域预测的自适应方法。
基于转移熵的时间序列因果分析 基于转移熵的时间序列因果分析
在本文中,我们讨论了如何将统计因果关系应用于识别预测变量。我们将探讨因果关系与传递熵(Transfer Entropy, TE)之间的联系,并展示用于检测两个变量之间信息方向性传递的MQL5代码。
自定义指标:为净额结算账户绘制部分入场、出场和反转交易 自定义指标:为净额结算账户绘制部分入场、出场和反转交易
在本文中,我们将探讨在MQL5中创建指标的一种非标准方法。我们的目标不是专注于趋势或图表形态,而是管理我们自己的仓位,包括部分入场和出场。我们将广泛使用动态矩阵以及一些与交易历史和未平仓头寸相关的交易函数,以在图表上显示这些交易发生的位置。