目录
简介
安排新元素测试
消息框(MessageBox)
选项卡控件(TabControl)
复选框(CheckBox)
单选按钮(Radio Button)
组合框(Combo Box)
数字选择(NumericUpDown, 数字列表窗口)
日期时间选择器(DataTimePicker, 日期选择窗口)
ElementHide 和 ElementEnable - 隐藏及禁用指定元素
AddItem - 添加子元素
Exception - 接收异常的事件
可用图形元素和事件的汇总表格
结论
简介
从 2018 年10月开始, MetaTrader 5 支持了与 .Net 框架库的整合。这组库实际上远不止是执行特定任务范围的框架或专用系统,例如绘制图形窗口或实现网络交互,.NET框架实际上拥有一切。它允许开发网站(Net Core,MVC),创建具有统一专业界面(Windows窗体)的系统应用程序,构建复杂的分布式系统,在节点之间进行数据交换,以及使用数据库(实体框架)。此外,.NET框架是一个由程序员和公司组成的庞大社区,拥有数千个不同的开源项目。如果交互是正确组织的,那么所有这些都可以在今天的MQL中使用。
在本文中,我们将继续开发在第一部分中创建的 GuiController 的功能。此功能旨在与基于Windows窗体技术的 .NET框架的图形功能交互。目前,在MQL图形功能上提供了大量信息,有许多不同的库通过MQL或多或少地做相同的事情。因此,我不希望读者将此材料视为“使用表单的另一个库”。事实上,本材料只是描述与.NET框架交互的一系列文章的一部分,并逐渐揭示了该软件平台的无限特性。Windows窗体只是这个平台中的一个构建块,尽管它非常方便和全面,就像.NET技术的任何一部分一样。Windows窗体图形子系统是探索此框架的一个很好的起点,经过适当的研究,它可以应用于与 .NET 框架的其他交互。此外,它还允许创建相当高效且最重要的是易于实现的交易面板、EA配置窗口、高级图形指标、机器人控制系统以及用户与交易平台之间交互的其他相关内容。
然而,为了实现所有这些令人兴奋的特性,有必要显著改进MQL程序和C#库之间的交互模块。您可能还记得,在第一节中,GuiController 模块只能与几个 WinForms 元素交互,例如按钮(Button)、文本标签(Label)、用于输入文本的文本字段(TextBox)和垂直滚动条。尽管支持很少,我们还是成功地创建了一个完整且功能相当强大的图形面板:
图 1. 文章第一部分创建的交易面板
尽管取得了令人印象深刻的成绩,但我们不会就此止步,继续改进我们的控制器。在本文中,我们将为它提供额外的图形元素,允许用户创建大多数类型的表单。
安排新元素测试
为了引入对新元素的支持,需要组织一种“测试台”。这使我们能够微调新元素的使用,并消除引入新功能时出现的潜在错误。我们的“测试台”由控制器、带有必要图形元素集的表单和用于处理这些元素的EA组成。所有窗体都将位于单个 DemoForm.exe 中。在EA中,我们将开发一个自定义参数,指定从DemoForm.exe下载的图形表单:
图 2. 选择下载的带有必要元素的自定义表单
测试EA本身非常简单,实际上,它由两部分组成:下载函数(标准的OnInit初始化函数)和图形事件处理程序函数(OnTimer函数中的事件循环)。您可能还记得,GuiController中的工作是通过调用静态方法来执行的。只有四种主要方法:
ShowForm - 从指定构建中载入一个表单;
HideForm - 隐藏表单;
GetEvent - 从表单中取得一个事件;
SendEvent - 向表单发送一个事件.
在OnInit函数中,我们将根据所选元素下载必要的窗口。函数原型如下:
int OnInit() { switch(ElementType) { case WINFORM_TAB: GuiController::ShowForm("DemoForm.exe", "tab_form"); break; case WINFORM_BUTTON: GuiController::ShowForm("DemoForm.exe", "button_form"); break; ... } ... }
在 OnTimer 函数中,我们将处理来自以下表单的事件:
//++//| 计时器函数 |//++void OnTimer() { // 根据计时器取得新的事件 for(static int i = 0; i < GuiController::EventsTotal(); i++) { int id; string el_name; long lparam; double dparam; string sparam; GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam); ... if(id == TabIndexChange) printf("Selecet new tab. Index: " + (string)lparam + " Name: " + sparam); else if(id == ComboBoxChange) printf("组合框 '" + el_name + "' 已改变 " + sparam); ... } }
这样,对于每个图形元素,我们将创建一个简短的工作示例,说明与它的交互。我们还将详细描述它支持的事件。
消息框(MessageBox)
从第二个版本开始,控制器支持消息框,这是一个标准的用户信息元素。它还向用户提供多个选项,并以所选选项的形式接收响应。
要启动消息窗口的演示,请在启动EA时选择 Windows 窗体元素类型参数中的“按钮和消息框(Buttons and MessageBox)”选项。启动EA后,会出现一个表单,提示您选择以下选项之一:
图 3. 调用消息框的示例窗体
这个窗体以及随后的所有窗体都是示范形式,因此不具备交易逻辑。但是,按下任何按钮后,EA会发送一条警告消息,请求确认所选操作。例如,单击“卖出(SELL)”时将显示以下消息窗口:
图 4. 交易EA要求确认开立新的空头头寸
用户单击其中一个按钮后,单击事件将被记住并记录在GuiController事件缓冲区中。EA以指定的频率调查事件缓冲区,并在发现新事件进入缓冲区后立即开始处理它。这样,EA需要接收“点击按钮(Button clicking)”事件,并通过发送 “MessageBox”对到来的事件做出反应。
for(static int i = 0; i < GuiController::EventsTotal(); i++) { int id; string el_name; long lparam; double dparam; string sparam; // 取得一个新的事件 GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam); // 定义它的类型 - button clicking if(id == ClickOnElement) { // 在终端控制台上显示点击按钮的名称 printf("您按下了 '" + sparam + "' 按钮"); string msg; // 根据点击按钮的类型,构建 MessageBox 消息 if(el_name != "btnCancelAll") msg = "您是否想要开立一个新的 " + sparam + " 仓位?"; else msg = "您是否确定关闭所有仓位?"; // 使用 MessageBox 显示命令发送正在进行的事件 GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OKCancel, msg); } ... }
让我们分析发送事件的签名:
GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OKCancel, msg);
这意味着EA要求显示消息框,其中“msg”变量中的主文本显示“确定”和“取消”按钮(OKCancel)。在本文的第一部分中,我们提到SendEvent方法的第一个参数包含发送事件接收器的图形元素的名称。但是,此字段与MessageBox的工作方式不同。消息框没有绑定到特定的图形窗口或元素(尽管Windows Froms允许这样的绑定)。因此,GuiController创建新的消息窗口,它不需要目标地址。通常,在显示消息后,应该阻止与之相关的窗口。事实上,如果在显示消息时,可以重复单击“买入”或“卖出”按钮而忽略出现的消息框,则会很奇怪。因此, 在GuiController中,此事件的第一个参数的名称代表应被阻止的元素,直到用户单击其中一个MessageBox按钮。任意图形元素的阻塞函数是可选的。它是使用整数lparam变量指定的:0-不阻挡窗口,1-如果存在就阻挡。但是,使用常量而不是零和一更方便。为此,使用 BlockingControl枚举在 GuiController中定义两个常量:
LockControl;
NotLockControl
第一个选项在用户按下按钮之前阻止窗口,第二个选项不允许用户访问图形窗口。
除了文本,消息窗口可以包含各种按钮的组合,通过单击这些按钮,用户同意某种选择。按钮集是使用System.Windows.Forms.MessageBoxButtons系统枚举定义的。枚举元素对MQL用户不可用,因为它们是在外部生成中定义的。为了简化处理 GuiController的MQL程序员的工作,我们实现了新的枚举——具有相同参数的System.Windows.Forms.MessageBoxButtons的*。枚举是在 IController.cs 中定义的:
//// 概述:// 定义那些按钮在 System.Windows.Forms.MessageBox 中显示的常数.public enum MessageBoxButtons { // // 概述: // 消息框包含 OK (确定)按钮。 OK = 0, // // 概述: // 消息框包含 OK (确定)和 Cancel (取消)按钮。 OKCancel = 1, // // 概述: // 消息框中包含 Abort(退出), Retry(重试)和 Ignore (忽略)按钮。 AbortRetryIgnore = 2, // // 概述: // 消息框中包含 Yes(是), No(否), 和 Cancel (取消)按钮。 YesNoCancel = 3, // // 概述: // 消息框中包含 Yes (是)和 No (否)按钮。 YesNo = 4, // // 概述: // 消息框中包含 Retry (重试)和 Cancel (取消)按钮。 RetryCancel = 5}
这些枚举的常量可以直接在MQL编辑器中使用,例如通过 IntelliSens,使得配置MessageBox非常方便。例如,如果我们在 SendEvent中将OKCancel替换为YesNoCancel,对话框窗口将接收另一组按钮:
GuiController::SendEvent("ButtonForm", MessageBox, LockControl, YesNoCancel, msg);
图 5. 标准的三按钮组合 - Yes/No/Cancel
除了按钮集,GuiController还支持配置消息图标以及窗口标题文本。由于 SendEvent 方法具有固定数量的参数,因此通过它传递所有设置是很有问题的,因此找到了另一种解决方案。消息文本行可以使用“|”符号分为多个部分。在这种情况下,每个部分负责一个特定的附加参数。节数可能从一个(无分隔符)到三个(两个分隔符)不等。让我们探讨几个例子,假设您希望显示一条不带图标或附加标题的简单消息,在这种情况下,消息发送格式如下:
GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OK, "This is a simple message");
图 6. 窗口名称中没有图标和附加文本的简单消息
可以使用附加部分中的特殊常量将图标添加到消息中。假设我们想要显示一条带有警告图标的消息。为此,请将消息文本格式替换为以下格式:
GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OK, "Warning|Your action can be dangerous");
图 7. 带有警告图标的消息
图标不仅可以用关键字设置,还可以用假名图标设置。如果我们输入“?”而不是警告,效果如下。除了警告,我们还可以设置信息、问题和错误图标。下面是图标的关键字和假名表:
图标 | 关键字 | 假名 |
---|---|---|
Warning | ! | |
Error | !!! | |
Info | i | |
Question | ? |
除了图标,我们还可以设置消息窗口的名称。为此,请用“|”分隔文本并输入窗口名称。以下是包含错误消息的窗口的完整定义示例:
GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OK, "!!!|操作取消|严重错误");
图 8. 带有错误图标的命名消息窗口
控制器具有智能线路分析功能。如果我们输入“!!!!|操作已取消”,将显示带有适当消息的严重错误图标。如果在行中指示两个部分“操作被取消|严重错误”,则不显示图标,但窗口名称更改为严重错误。
选项卡控件(TabControl)
选项卡是将元素分组的方便工具:
图 9. 含有两个选项卡的面板
选项卡控件元素支持单个TabIndexChange事件,它通知用户已移动到另一个选项卡。测试EA以表单上的代码跟踪表更改为特征。让我们看看代码片段:
for(static int i = 0; i < GuiController::EventsTotal(); i++) { int id; string el_name; long lparam; double dparam; string sparam; GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam); if(id == TabIndexChange) printf("选择了新页面. Index: " + (string)lparam + " Name: " + sparam); }
TabIndexChange 事件传递两个参数: lparam 和 sparam. 第一个包含用户选择选项卡的索引。第二个包含所选选项卡的名称。例如,如果用户选择第一个选项卡,则EA会键入以下消息:
选择新选项卡. 索引: 0 名称: tabPage1
选项卡是非常有用的图形元素,并不总是需要跟踪选项卡。WindowsForm要求单个窗体的所有元素具有唯一的名称。因此,位于不同选项卡中的同一类型的两个元素是唯一的,并且根据 WindForms,应以不同的方式命名。另一方面,通常需要跟踪直接控件的按下情况,这样一个元素所在的标签并不总是重要的。然而,有时有必要跟踪这些事件,因此,GuiController提供了足够用于此元素的必要交互接口。
复选框(CheckBox)
复选框是任何图形界面的关键元素之一。尽管它很简单,但是它被用于各种各样的界面,从旧版本的Windows开始,到Web和移动应用程序结束。它允许任何选项的直观指示。此外,还可以显示由于某种原因不可用的选项,允许用户直观地选择彼此不矛盾的选项:
图 10. 使用复选框组合选择选项
复选框有三种状态:选中(Checked)、未选中(Unchecked)和部分选中(Indeterminate)。Windows Forms 使用 System.Windows.Forms.CheckState 结构来描述这些状态:
namespace System.Windows.Forms { // // 概述: // 指定控件的状态,如复选框,可以选中、取消选中, // 或设置为不确定状态。 public enum CheckState { // // 概述: // 控件没有被选中 Unchecked = 0, // // 概述: // 控件被选中 Checked = 1, // // 概述: // 控件不确定状态,不确定状态的控件通常有阴影 // 外观 Indeterminate = 2 } }
每次用户单击此复选框时,GuiController都会通过lparam变量使用 CheckBoxChange 事件将其状态传递给MQL EA。它的数值对应了枚举中的选项之一: 0 — Unchecked, 1 — Checked, 2 — Indeterminate.
在演示示例中,EA跟踪“启用EURUSD交易”和“启用GBPUSD交易”复选框的选择。一旦其中一个选项可用,它就会解锁其“允许获利”和“允许止损”子选项。相反,如果用户从其中一个主选项中删除标志,则其子选项将立即锁定。这要归功于两个事件:ElementEnable和CheckBoxChange。下面的代码介绍了EA处理复选框的算法:
void OnTimer() { // 根据计时器取得新的事件 for(static int i = 0; i < GuiController::EventsTotal(); i++) { int id; string el_name; long lparam; double dparam; string sparam; GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam); if(id == CheckBoxChange) ChangeEnableStopAndProfit(el_name, id, lparam, dparam, sparam); } }//++//| 修改启用止损和获利 |//++void ChangeEnableStopAndProfit(string el_name, int id, long lparam, double dparam, string sparam) { int id_enable = ElementEnable; if(el_name == "EURUSDEnable") { GuiController::SendEvent("EURUSDProfit", id_enable, lparam, dparam, sparam); GuiController::SendEvent("EURUSDStop", id_enable, lparam, dparam, sparam); } else if(el_name == "GBPUSDEnable") { GuiController::SendEvent("GBPUSDProfit", id_enable, lparam, dparam, sparam); GuiController::SendEvent("GBPUSDStop", id_enable, lparam, dparam, sparam); } }
一旦通知EA用户选中了其中一个复选框,它就会将当前的 ElementEnable事件“true”发送给GuiController。相反,如果用户取消选中该框,则发送ElementEnable等于'false'。由于EA和表单在不同事件的帮助下进行了交互,因此创建了一种交互效果:表单开始根据用户选择更改子元素的可访问性,尽管控制逻辑本身直接位于EA中。
单选按钮(Radio Button)
单选按钮是一个简单的图形元素,允许用户从预定义的点中选择必要的点:
图 11. 单选按钮
当用户更改他们的选择时,EA会收到两次更改事件:来自未选中按钮和来自选中按钮。使用相同的RadioButtonChange标识符跟踪这两个事件。Here is an example of its use:
for(static int i = 0; i < GuiController::EventsTotal(); i++) { int id; string el_name; long lparam; double dparam; string sparam; GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam); else if(id == RadioButtonChange) { if(lparam == true) printf("您选择了 " + sparam); else printf("您取消选择了 " + sparam); } }
lparam参数包含一个标志,通知按钮发生了什么:它是被选中(plaram=true)还是未选中(lparam=false)。单击按钮时,EA在终端中显示类似的消息:
你已经取消选择了EA 您已经选择了指标 您已经取消选择了指标 您已经选择了脚本 ...
组合框(Combo Box)
组合框是最常见的元素之一。除了复选框之外,它还应用于Web开发和现代移动应用程序。它也是Windows中最常用的元素之一:
图 12. 组合框和可用菜单项
组合框用于两种主要模式。第一个允许用户输入除现有值之外的新值:
图 13. 选择能够输入新符号的符号
第二种模式只为用户提供预先定义的菜单项,而不能选择自定义菜单项(见图11)。还有第三种模式隐藏菜单项,但很少使用,所以我们不会停留在它上面。
所有组合框显示模式都是使用DropDownStyle属性设置的。此属性通常在开发图形界面时设置一次,因此guiController没有允许您更改组合框类型的事件。但是,控制器允许跟踪组合框中元素的选择,以及输入新值。因此,ComboBox支持两个事件:它自己的ComboBoxChange和TextChange。我们的演示表单由两个组合框元素组成。第一个选项提供选择平台(MetaTrader 4/MetaTrader 5),第二个选项选择交易品种。第二个元素在默认情况下被阻止。然而,一旦用户选择了一个平台,他们就可以选择一个交易符号。下面是实现该功能的代码:
for(static int i = 0; i < GuiController::EventsTotal(); i++) { int id; string el_name; long lparam; double dparam; string sparam; GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam); if(id == ComboBoxChange) { if(el_name == "comboBox1") // 用户选择平台后立即取消阻止交易品种列表: GuiController::SendEvent("comboBox2", ElementEnable, 1, 0.0, ""); printf("组合框 '" + el_name + "' 已改变 " + sparam); } }
如果我们开始选择组合框元素,演示 EA 将开始在终端中显示用户选择的参数:
在 MetaTrader 5 中组合框 “comboBox1" 已经改变 在 GBPUSD 上组合框 'comboBox2' 已经改变 在 USDJPY 上组合框 'comboBox2' 已经改变 ...
数字选择(NumericUpDown, 数字列表窗口)
数字列表窗口通常用于分析系统,包括交易面板。这就是为什么这个元素是第一个包含在 GuiController中的元素。数字列表窗口允许设置控制输入类型的特定值,只能输入数字。可以使用特殊的小滚动按钮更改步长,也可以显示数字的小数位:
图 14. 数字列表窗口
GuiController 对这种元素类型支持四种事件:
NumericChange 接收或发送包含窗口新数值的事件;
NumericFormatChange 发送一个事件,指定数字的位数容量(在lparam变量中)及其更改步长(在dparam变量中);
NumericMaxChange 发送指定最大可能值的事件;
NumericMinChange 发送指定最小可能值的事件。
NumericUpDown 通过单一的 NumericChange 事件与用户交互。当用户更改此窗口中的数值时,EA将通过事件接收适当的通知。这是唯一可能的用户交互。但是,EA可以配置窗口设置最重要的参数:小数位数、更改步长以及最大和最小可接受值。所有这些参数都取决于EA逻辑及其使用的数据类型,因此,不可能在表单设置中直接定义它们。它们应该在程序启动期间定义。
测试EA包括一个小示例,说明如何使用NumericUpDown。下面提供了从图13上传表单的代码。
//++//| EA 交易初始化函数 |//++int OnInit() { if(ElementType != WINFORM_HIDE) EventSetMillisecondTimer(100); else EventSetMillisecondTimer(1000); switch(ElementType) { ... case WINFORM_NUMERIC: { GuiController::ShowForm(assembly, "NumericForm"); double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK); double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID); double price_step = NormalizeDouble(SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_SIZE), Digits()); long digits = (long)Digits(); GuiController::SendEvent("NumericForm", TextChange, 0, 0.0, "NumericForm (" + Symbol() + ")"); NumericSet("StopLoss", Digits(), ask, (double)LONG_MAX, 0.0, price_step); NumericSet("TakeProfit", Digits(), bid, (double)LONG_MAX, 0.0, price_step); break; } ... } return(INIT_SUCCEEDED); }
从代码中可以看出,在上传过程中,EA接收到当前交易品种上的数据:其询问和投标价格、小数位数和价格步长。然后,使用特殊的辅助 NumericSet 函数为 NumericUpDown表单元素设置这些参数。让我们看一下它的代码:
//++//| 设置 NumericUpDown 参数 |//| name - NumericUpDown 元素的名称 |//| digits - 交易品种的小数位数 |//| init - 初始化双精度值 |//| max - 最大值 |//| min - 最小值 |//| step - 变化步长 |//++void NumericSet(string name, long digits, double init, double max, double min, double step) { int id_foramt_change = NumericFormatChange; int id_change = NumericChange; int id_max = NumericMaxChange; int id_min = NumericMinChange; long lparam = 0; double dparam = 0.0; string sparam = ""; GuiController::SendEvent(name, id_max, lparam, max, sparam); GuiController::SendEvent(name, id_min, lparam, min, sparam); GuiController::SendEvent(name, id_change, lparam, init, sparam); GuiController::SendEvent(name, id_foramt_change, digits, step, sparam); }
代码可以自适应地工作,根据它所运行的交易品种,我们将看到不同的价格格式:
图 15. 每个交易品种的单独价格格式
日期时间选择器(DataTimePicker, 日期选择窗口)
此元素在概念上与 NumericUpDown类似,唯一的区别是它允许用户安全地选择日期,而不是数字:
图 16. 在 DateTimePicker元素中选择精确时间
与 NumericUpDown 元素相比,与 DateTimePicker 的交互更加简单,这是因为,与数字格式不同,数字格式取决于EA当前的交易环境,日期格式或多或少具有通用性。它可以在开发表单时设置,然后保持完整。因而 DataTimePicker 支持唯一的 DateTimePickerChange 事件,通过 lparam 参数传递和接收准确的日期。这里是使用元素的一个例子:
for(static int i = 0; i < GuiController::EventsTotal(); i++) { int id; string el_name; long lparam; double dparam; string sparam; GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam); if(id == DateTimePickerChange) printf("用户设置了新 datetime 值: " + TimeToString((datetime)lparam)); }
如果我们运行演示 EA 并把 DateTimePicker 作为演示元素,窗口类似于图15中出现的窗口,如果我们开始以各种方式更改日期和时间,则EA将响应这些事件,并在日志中显示新的日期和时间值:
User set a new datetime value: 2019.05.16 14:21User set a new datetime value: 2019.05.16 10:21User set a new datetime value: 2021.05.16 10:21
然而,与元素的交互比看起来要复杂一些,MQL 和 C# 有不同的时间格式,MQL采用简化的POSIX时间格式,分辨率为1秒,最早的可能日期为1970.01.01,而C#采用更高级的时间格式,分辨率为100纳秒。因此,为了与不同的系统交互,我们需要开发将一种时间格式转换为另一种时间格式的时间转换器。在 GuiController 中就使用了这样的转换器,它设计为 MtConverter 公共静态类:
/// <summary>/// MetaTrader - C# 系统转换器 /// </summary>public static class MtConverter { /// <summary> /// 把 C# 日期时间格式改为 MQL (POSIX) 日期时间格式 /// </summary> /// <param name="date_time"></param> /// <returns></returns> public static long ToMqlDateTime(DateTime date_time) { DateTime tiks_1970 = new DateTime(1970, 01, 01); if (date_time < tiks_1970) return 0; TimeSpan time_delta = date_time - tiks_1970; return (long)Math.Floor(time_delta.TotalSeconds); } /// <summary> /// 把 MQL (Posix) 时间格式转换为 C# 时间格式 /// </summary> /// <param name="mql_time">MQL 日期时间单位</param> /// <returns></returns> public static DateTime ToSharpDateTime(long mql_time) { DateTime tiks_1970 = new DateTime(1970, 01, 01); if (mql_time <= 0 || mql_time > int.MaxValue) return tiks_1970; TimeSpan time_delta = new TimeSpan(0, 0, (int)mql_time); DateTime sharp_time = tiks_1970 + time_delta; return sharp_time; } }
目前,它只包含两个方法:ToMqlDateTime 将 DateTime转换为 MQL。第二个方法将MQL时间值转换为C# DateTime结构,结果正好相反。由于datetime(mql)和 datetime(C#)类型彼此不兼容,因此转换是通过常规的“long”类型执行的,这对于所有系统都是相同的。因此,在接收到 DateTimePikerChange 事件时,我们需要显式地将lparam转换为datetime以接收正确的时间值:
// 显式将 long 值转换为datetime。它是完全安全的printf("User set new datetime value: " + TimeToString((datetime)lparam));
我们不仅可以获得新的值,而且可以用类似的方式自己设定它们。例如,我们可以使用以下命令设置当前终端时间:
GuiController::SendEvent("DateTimePicker", DateTimePickerChange, ((long)TimeCurrent()), 0.0, "");
ElementHide 和 ElementEnable - 隐藏及禁用指定元素
有一些通用事件允许您控制任何 WinForms 元素,其中就有 ElementHide 和 ElementEnable 事件。如需隐藏元素,按如下方法使用 ElementHide :
GuiController::SendEvent("HideGroup", ElementHide, true, 0.0, "");
其中 HideGroup 是需要隐藏的元素名称,如需显示元素,就做对应的调用:
GuiController::SendEvent("HideGroup", ElementHide, false, 0.0, "");
请注意所使用元素的名称,WindowsForm中的单个元素可能包含内部元素,这被称为元素嵌套(elements nesting)。这样的安排允许在组级别管理所有元素。在示例中,文本框与嵌套在其中的“label”标题一起使用。在给定的频率下,框中的所有元素都将消失,然后再次出现:
图 17. 从EA中隐藏任意图形元素
此外,可以使每个元素不可用,虽然它不会消失,但使用它变得不可使用。这是一个有用的选项,允许设计更复杂和直观的界面。我们已经在标志描述中提到使用此事件。可以通过发送以下事件使元素不可用:
GuiController::SendEvent("element", ElementEnable, false, 0.0, "");
只需将“false”标志替换为“true”,即可再次激活元素:
GuiController::SendEvent("element", ElementEnable, true, 0.0, "");
AddItem — 添加子元素
某些元素可能包含其他元素,在程序启动之前,这些子元素的内容通常是未知的。假设我们需要显示一个交易品种列表,这样用户就可以选择他们需要的交易品种。出于这些目的,最合理的做法是使用组合框:
图 18. 预先设置的交易品种列表
但是,在编译图形表单的阶段不能提前输入交易品种,因为可用交易品种的列表可能因代理而异。因此,此类型的内容应动态形成。使用的命令是 AddItem。最简单的方法是列出 MarketWatch 中所有可用的交易品种,并将其作为菜单项添加:
//++//| EA 交易初始化函数 |//++int OnInit() { EventSetMillisecondTimer(100); GuiController::ShowForm(assembly, "SendOrderForm"); for(int i = 0; i < SymbolsTotal(true); i++) { GuiController::SendEvent("SymbolComboBox", AddItem, 0, 0, SymbolName(i, true)); } return(INIT_SUCCEEDED); }
如您所见,这个命令非常容易使用。尽管该命令是通用的,但并非所有元素都允许向其添加子元素,目前,GuiController仅支持组合框的此事件,然而,这足以构建高效的图形界面。此列表将来可能会扩展。
Exception — 接收异常的事件
我们不可能总是写没有错误的程序,此外,在一些情况下,可能在无法预知的场景下程序执行时会出现错误。在这些情况下,能够获得所发生的数据的反馈是必要的。Exception 事件就是为了这种交互而提供的,它是由 GuiController 本身生成,再发送给到 MQL EA 中的。GuiController 在两种情况下会创建错误消息:
在系统异常拦截的情况下;
如果从EA发送的事件不支持为其选择的元素,或者事件本身具有无效值。
让我们逐个分析这些选项。为了演示第一个选项,让我们返回演示代码,即显示 NumericUpDown元素的选项。在这个启动选项中,调用一个特殊的 NumericSet函数。它会设置 NumericUpDown 元素的工作范围:
void NumericSet(string name, long digits, double init, double max, double min, double step) { int id_foramt_change = NumericFormatChange; int id_change = NumericChange; int id_max = NumericMaxChange; int id_min = NumericMinChange; long lparam = 0; double dparam = 0.0; string sparam = ""; // GuiController::SendEvent(name, id_max, lparam, max, sparam); GuiController::SendEvent(name, id_min, lparam, min, sparam); GuiController::SendEvent(name, id_change, lparam, init, sparam); GuiController::SendEvent(name, id_foramt_change, digits, step, sparam); }
此函数已被轻微更改,因此未设置元素的最大值(其中的相应字符串被注释掉),如果在黄金图表上编译后运行此表单,它会突然停止显示当前价格:
图 19. 价格设置表单中的零值
发生了什么?异常系统是用来回答这个问题的。每个异常都可以通过 GuiController::GetEvent获得,就像其他消息一样:
// 根据计时器取得新的事件for(static int i = 0; i < GuiController::EventsTotal(); i++) { int id; string el_name; long lparam; double dparam; string sparam; GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam); ... if(id == Exception) printf("Unexpected exception: " + sparam); }
消息输出已经说了很多:
意外异常:“1291,32”值对于“Value”是不可接受的,"Value”应在“Minimum”到“Maximum”的范围内。 参数名称: Value 意外异常: '1291,06' 对于 'Value' 是不可接受的,"Value”应在“Minimum”到“Maximum”的范围内。 参数名称: Value
实际情况是 NumericUpDown 默认的工作范围是从 0 到 100,因此,设置黄金的当前价格(每金衡盎司1292美元)会导致错误。要避免这种情况,请使用NumericMaxChange事件通过程序扩展可接受值的范围。这就是 NumericSet 函数中注释掉的代码所做的。
当发送错误事件时,调用异常的第二个选项是可能出现的,例如,可以将事件发送到不存在的目标:
GuiController::SendEvent("empty", ElementEnable, 0, 0.0, "");
回应将如下:
意外异常: SendEvent:未找到名为“empty”的元素
我们还可以尝试发送不支持该项目的事件。例如,让我们尝试将以下文本添加到“止损水平”输入字段(NumericUpDown元素类型):
GuiController::SendEvent("StopLoss", AddItem, 0, 0.0, "New Text");
答案将非常简洁:
意外异常:元素“StopLos”不支持“Add Item”事件
异常系统为创建复杂的图形应用程序提供了宝贵的帮助,这种应用程序中的错误是不可避免的,开发速度和方便程度取决于程序员识别它们的速度。
可用图形元素和事件的汇总表格
将支持的图形元素系统化,与 GuiController协同工作是合理的。为此,我们创建一个表,其中包含有关这些元素的摘要信息以及在GuiController中使用它们的方法。“Sample usage(示例用法)”列包含一个简短的示例代码,说明如何从MQL程序中使用此元素。所讨论的代码应被视为使用元素的总体模式的一部分。例如,第一个示例的代码(MessageBox):
string msg = "!!!|操作被取消|严重错误"; GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OK, msg);
应当在下面的环境中考虑:
void OnTimer{ //... // 根据计时器取得新的事件 for(static int i = 0; i < GuiController::EventsTotal(); i++) { int id; string el_name; long lparam; double dparam; string sparam; GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam); if(id == MessageBox) { string msg = "!!!|操作被取消|严重错误"; GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OK, msg); } } }
该模式在其他示例中也以类似的方式使用。
图形元素 | 元素或事件的名称 | 主要事件 IDs | 示例用法 |
---|---|---|---|
消息框(MessageBox) | 消息框(MessageBox) | string msg = "!!!|操作被取消|严重错误"; GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OK, msg); | |
页面(Tabs) | TabIndexChange | GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);printf("选择了新页面. 索引: " + (string)lparam + " Name: " + sparam); | |
复选框(CheckBox) | CheckBoxChange | GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);printf("选择了" + sparam + " " + lparam); | |
单选按钮(RadioButton) | RadioButtonChange | GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);if(lparam == true) printf("您选择了 " + sparam);else printf("您取消选择了 " + sparam); | |
组合框(ComboBox) | ComboBoxChange | GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);printf("组合框 '" + el_name + "' 已改变 " + sparam); | |
数字调节(NumericUpDown) | NumericChange NumericFormatChange NumericMaxChange NumericMinChange | GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);printf("数值 '" + el_name + "' 已改变, 新值: " + DoubleToString(dparam, 4)); | |
日期时间选择器(DateTimePicker) | DateTimePickerChange | GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);printf("用户设置了新 datetime 值: " + TimeToString((datetime)lparam)); | |
Vertical Scroll | ScrollChange | GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);printf("垂直滚动部分有了新数值: " + (string)lparam); | |
文本框(TextBox) | TextChange | GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);printf("输入了新数值: " + sparam); | |
按钮(Button) | ClickOnElement | GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);printf("按钮 " + sparam + " 被按下"); | |
标签(Label) | TextChange | GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);printf("标签有了新的文字: " + sparam); |
除了图形元素之外,GuiController还支持使用它们的通用事件。让我们以表格的形式来安排这些事件:
事件 | 描述 | 示例用法 |
---|---|---|
ElementHide | 隐藏/恢复一个元素 | GuiController::SendEvent("HideGroup", ElementHide, true, 0.0, ""); |
ElementEnable | 启用/禁用一个元素 | GuiController::SendEvent("HideGroup", ElementEnable, true, 0.0, ""); |
AddItem | 把新的子元素加到所选元素中 | GuiController::ShowForm(assembly, "SendOrderForm");for(int i = 0; i < SymbolsTotal(true); i++) GuiController::SendEvent("SymbolComboBox", AddItem, 0, 0, SymbolName(i, true)); |
异常 | 取得在调用 CLR 时的异常 | GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);printf("Unexpected exception: " + sparam); |
结论
我们分析了Windows窗体的主要图形元素以及与之交互的示例。这些元素很少,但它们代表了任何图形应用程序的主干。尽管它们不包括表格(交易中另一个非常重要的元素),但是您仍然可以使用它们来创建功能性的图形应用程序。
最新的 GuiController 版本附在下面,此外,还可以从GitHub存储库系统复制此版本。可以在此处找到库版本: . 您还可以复制示范窗体项目。它位于 . 阅读 文章的第一部分,了解如何通过版本控制系统获得最新的库版本。所附文件包含所有项目的完整源代码。其中有三个:EA调用带有元素的窗体,DemoForm.exe 构建中的一组表单,编译的 Guicontroller(source\mql5\libraries\guicontroller.dll)以及一个源代码(source\sharp\guicontroller)。另外,请注意,演示 EA 需要到已启动表单的绝对路径。在您的PC上,它将与 “assembly” EA参数中指定的不同,因此将其替换为实际路径。