
在 MQL5 中创建交互式图形用户界面(第 2 部分):添加控制和响应
概述
在上一篇文章中,我们通过编写 MetaQuotes Language 5(MQL5) 图形用户界面 (GUI) 面板的图形元素奠定了基础。如果你还记得,迭代是图形用户界面元素的静态组合 - 仅仅是定格在时间中的快照,缺乏响应性。它是静止的、没有反应的。现在,让我们解冻快照,为其注入活力。在这部万众期待的续集中,我们将把面板讨论推向新的高度。准备好,让我们一起探索如何为界面注入活力:
- 布局和响应性:忘掉静态组件吧!我们将采用相对定位方法、灵活的布局以及可点击、可响应和可编辑的组件,使我们的面板能够响应用户的交互。
- 动态更新:实时数据是每个交易应用程序的核心。我们将深入获取实时价格信息,并确保我们的面板反映最新的市场信息。
- 组件移动性:想象一下可拖动元素 - 可对用户的触碰做出反应的面板。我们将探索如何使某些组件可移动,从而增强用户体验。
以下主题将指导我们如何建立一个反应灵敏、互动性强的面板:
- 待自动化元素示意图
- MQL5 中的图形用户界面自动化
- 结论
待自动化元素示意图
有七个组件将实现自动化。第一个组件是单击关闭按钮时面板的关闭。我们打算在单击此按钮时删除所有面板元素。其次,当点击仓位管理按钮时,按钮将根据指示关闭各自的仓位和订单。例如,当我们点击 "盈利" 按钮或标签时,就会关闭所有盈利的仓位。第三个自动化将针对交易量组件。单击它的实体后,将创建一个选项的下拉列表,供用户选择交易选项。
第四项自动化是在相应交易按钮旁边的增加或减少按钮上,它们可以增加或减少编辑栏中的值,而不是直接键入。如果用户想直接输入所需的值,编辑栏就需要捕捉输入的值,这就是我们的第五个自动化步骤。然后,第六步将是在悬停按钮上创建悬停效果。也就是说,当鼠标位于悬停按钮区域内时,按钮会变大,表明鼠标位于按钮附近;当鼠标离开按钮区域时,按钮会重置为默认状态。最后,我们会在每个价格分时上将报价更新为实时值。
为了便于理解这些自动化流程和组件,下文将详细介绍这些流程和组件在上一个里程碑中的特点。
了解我们将要做的事情后,让我们立即开始自动化。如果您还没有阅读上一篇文章,请参阅我们创建图形用户界面元素静态组件的文章,这样您就能与我们一起步入正轨。让我们开始吧。
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 位数,这在数学上是有区别的。下面举例说明我们的意思。
正如我们所看到的,类型转换可以消除警告,但在精确度很重要的情况下,这并不是最好的方法。因此,需要另一个函数。我们使用 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 图片中,就是我们目前取得的成果。
这就是我们需要在事件处理函数上做的全部工作,负责更新价格的完整源代码如下:
//+------------------------------------------------------------------+ //| 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 中,一切都应该清楚了。现在我们开始捕捉 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 并在必要时采取行动。下面是逻辑的图解:
点击救护车图标后,第一个组件自动化功能将在 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 函数。该函数用于强制重绘指定图表。以编程方式修改图表属性或对象(如指标、线条或形状)时,更改可能不会立即反映在图表上。通过调用它,可以确保图表更新并显示最新的变化。下面是我们得到的直观结果。
由此可见,逻辑非常简单,其它对象的点击也采用同样的方法。现在让我们继续处理点击关闭按钮标签时发生的事件。发生这种情况时,我们需要关闭所有未平仓位,删除所有挂单。这将确保我们没有任何市场订单。需要一个 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 开发人员。
要删除挂单,也要使用相同的迭代逻辑。
// 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); }
建议每次在面板上添加逻辑后,都要编译并运行代码,以确保在升级到另一个控制逻辑之前,一切都能按预期运行。这就是我们目前取得的成就。
现在,我们可以成功关闭所有仓位和订单。请注意,当点击关闭按钮时,仓位正在关闭,按钮的标签颜色保持红色,直到所有仓位都关闭,最后恢复原来的颜色。同样,您可以注意到,我们没有关闭 "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); }
以下是本里程碑的可视化展示。
如图所示,很明显,我们的面板标题按钮现在点击时会有反应。现在,我们开始为交易量按钮增添活力。我们希望,当单击按钮或标签本身或单击下拉图标时,我们会创建另一个子面板,其中包含用户可以选择的各种选项列表。逻辑如下:
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(); }
测试结果如下:
到目前为止,一切都在按预期进行。用户可以选择不使用增加和减少按钮,而是直接使用编辑按钮字段中的编辑选项。在此过程中,编辑时可能会出现不可预见的错误,导致操作被忽略。例如,用户可能输入 "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。
后面的内容如下。
同样的逻辑也适用于拆分字符串的第二部分,即分隔符右侧的子串上。其源代码如下。
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"(错误)。为了吸引用户的最终注意力,我们将文字颜色设置为白色,背景颜色设置为红色,这种醒目的颜色组合让用户很容易就能识别出出现了错误。编译后,我们得到了以下结果。
至此,大部分面板组件的自动化工作已经完成。唯一没有考虑到的是下拉列表的移动和鼠标在按钮上的悬停效果。当鼠标在图表上移动时,所有这些都需要考虑,因此将考虑 "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; } }
经过编译,我们得到了以下结果:
真是太好了。现在,我们进入了最后一部分,即不仅要跟踪鼠标光标的移动,还要随之移动对象或组件。同样,我们声明了一个静态整数变量来检测鼠标何时被点击,以及一个布尔变量来存储鼠标光标的移动状态。这可以通过下面的代码片段实现。
// 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 函数来禁用图表滚动标志。这将确保鼠标移动时,图表不会水平滚动。这样,只有鼠标光标会随着指定对象移动。最后,我们设置新对象的距离,涉及当前鼠标坐标,并重新绘制图表使更改生效。一言以蔽之,这就是我们现在的情况:
现在你可以看到,我们可以拖动图标了。不过,我们还需要将它与其他面板组件拖到一起。因此,同样的逻辑也适用。
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);
添加其它元素的拖动逻辑将确保拖动图标移动时,其他面板组件也随之移动。编译后,我们得到了这样的结果:
成功了,您可以看到,所有面板组件都会随着鼠标光标移动。不过,我们还需要解决一个小问题。当鼠标被释放时,即不在按压模式下,组件会随着光标的移动而继续移动。要使面板脱离移动状态,我们需要在鼠标未按下时将状态设置为 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; }
一言以蔽之,这就是我们的成果。
这太棒了,我们刚刚为我们的图形用户界面面板注入了活力,现在我们的面板是可交互和响应的。它有悬停效果、按钮点击、实时数据更新,并能对鼠标移动做出反应。
结论
总之,通过本文的实现,我们可以说,将动态功能集成到 MetaQuotes Language 5(MQL5) 图形用户界面面板中,可以使其更具交互性和功能性,从而显著增强用户体验。添加按钮悬停效果可以创建一个视觉上引人入胜的界面,直观地响应用户操作。买卖价格的实时更新确保交易者拥有最新的市场信息,使他们能够快速做出明智的决策。用于执行买卖订单的可点击按钮,以及仓位和订单关闭功能,简化了交易操作,使用户能够及时对市场变化做出反应。
此外,可移动子面板和下拉列表的实现为界面增加了一层定制和灵活性。交易员可以根据自己的喜好组织工作空间,提高整体效率。下拉列表功能提供了一种方便的方式来访问各种选项,而不会使主界面变得混乱,从而有助于打造一个更清洁、更有组织的交易环境。总之,这些增强功能将 MQL5 GUI 面板转变为一个强大、用户友好的工具,满足了现代交易者的需求,最终改善了他们的交易体验和效率。交易者可以使用所示的知识来创建更复杂、更吸引人的 GUI 面板,以改善他们的交易体验。我们衷心希望您发现这篇文章详细、客观、易于理解和学习。干杯!
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/15263



