基于 .Net 框架和 C# 开发图形界面(第二部分):另外的图形元素

27 六月 2019, 08:55
Vasiliy Sokolov
0
1 650

目录


简介

从 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中的工作是通过调用静态方法来执行的。只有四种主要方法:

  1. ShowForm - 从指定构建中载入一个表单;
  2. HideForm - 隐藏表单;
  3. GetEvent - 从表单中取得一个事件;
  4. 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支持两个事件:它自己的ComboBoxChangeTextChange。我们的演示表单由两个组合框元素组成。第一个选项提供选择平台(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:21
User set a new datetime value: 2019.05.16 10:21
User 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 在两种情况下会创建错误消息:

  1. 在系统异常拦截的情况下;
  2. 如果从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存储库系统复制此版本。可以在此处找到库版本: https://github.com/PublicMqlProjects/MtGuiController.git. 您还可以复制示范窗体项目。它位于 https://github.com/PublicMqlProjects/GuiControllerElementsDemo.git. 阅读 文章的第一部分,了解如何通过版本控制系统获得最新的库版本。所附文件包含所有项目的完整源代码。其中有三个:EA调用带有元素的窗体,DemoForm.exe 构建中的一组表单,编译的 Guicontroller(source\mql5\libraries\guicontroller.dll)以及一个源代码(source\sharp\guicontroller)。另外,请注意,演示 EA 需要到已启动表单的绝对路径。在您的PC上,它将与 “assembly” EA参数中指定的不同,因此将其替换为实际路径。

本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/6549

附加的文件 |
Source.zip (50.14 KB)
轻松快捷开发 MetaTrader 程序的函数库(第五部分):交易事件集合类,向程序发送事件 轻松快捷开发 MetaTrader 程序的函数库(第五部分):交易事件集合类,向程序发送事件

在之前的文章中,我们已着手创建一个大型跨平台函数库,简化 MetaTrader 5 和 MetaTrader 4 平台上的程序开发。 在第四部分中,我们测试了在帐户上跟踪交易事件。 在本文中,我们将开发交易事件类,并将它们置于事件集合当中。 从那里,它们将被发送到 Engine (引擎)库的基准对象,并控制程序图表。

利用 MQL5 和 MQL4 实现的选择和导航工具: 把数据添加到图表中 利用 MQL5 和 MQL4 实现的选择和导航工具: 把数据添加到图表中

在本文中,我们将继续扩展实用程序的功能。这一次,我们将增加显示简化交易的数据的能力,特别是前一天的最高、最低价位,全年的最高、最低价位,开盘时间等。

在交易中应用 OLAP(第 1 部分):在线分析多维数据 在交易中应用 OLAP(第 1 部分):在线分析多维数据

本文论述如何创建多维数据(OLAP - 在线分析处理)的在线分析框架,以及如何在 MQL 中实现此框架,还有利用交易帐户历史数据在 MetaTrader 环境中应用此类分析的示例。

在交易中应用 OLAP(第 2 部分):可视化交互式多维数据分析的结果 在交易中应用 OLAP(第 2 部分):可视化交互式多维数据分析的结果

在本文中,我们会探讨为一个MQL程序创建一个交互式图形界面,该程序设计用于使用OLAP技术处理帐户历史和交易报告。为了获得视觉效果,我们将使用最大化和可伸缩的窗口、自适应布局的控件和用于显示图表的新控件。为了提供可视化功能,我们将实现一个GUI,其中沿着坐标轴选择变量,以及选择聚合函数、图表类型和排序选项。